diff --git a/contracts/dependencies/degods-staking/BaseWormholeBridgedNft.sol b/contracts/dependencies/degods-staking/BaseWormholeBridgedNft.sol new file mode 100644 index 000000000..d8959f0da --- /dev/null +++ b/contracts/dependencies/degods-staking/BaseWormholeBridgedNft.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {ERC2981Upgradeable} from "@openzeppelin/contracts-upgradeable/token/common/ERC2981Upgradeable.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {IWormhole} from "./wormhole-solidity/IWormhole.sol"; +import {BytesLib} from "./wormhole-solidity/BytesLib.sol"; +import {DummyERC721EnumerableUpgradeable} from "./DummyERC721EnumerableUpgradeable.sol"; + +/** + * @title DeBridge + * @notice ERC721 that mints tokens based on VAAs. + */ +abstract contract BaseWormholeBridgedNft is +UUPSUpgradeable, +DummyERC721EnumerableUpgradeable, +ERC2981Upgradeable, +Ownable2StepUpgradeable +{ + using BytesLib for bytes; + using SafeERC20 for IERC20; + + // Wormhole chain id that valid vaas must have -- must be Solana. + uint16 constant SOURCE_CHAIN_ID = 1; + + // -- immutable members (baked into the code by the constructor of the logic contract) + + // Core layer Wormhole contract. + IWormhole private immutable _wormhole; + // ERC20 DUST token contract. + IERC20 private immutable _dustToken; + // Only VAAs from this emitter can mint NFTs with our contract (prevents spoofing). + bytes32 private immutable _emitterAddress; + // Common URI for all NFTs handled by this contract. + bytes32 private immutable _baseUri; + uint8 private immutable _baseUriLength; + + // Amount of DUST to transfer to the minter on upon relayed mint. + uint256 private _dustAmountOnMint; + // Amount of gas token (ETH, MATIC, etc.) to transfer to the minter on upon relayed mint. + uint256 private _gasTokenAmountOnMint; + // Dictionary of VAA hash => flag that keeps track of claimed VAAs + mapping(bytes32 => bool) private _claimedVaas; + + error WrongEmitterChainId(); + error WrongEmitterAddress(); + error FailedVaaParseAndVerification(string reason); + error VaaAlreadyClaimed(); + error InvalidMessageLength(); + error BaseUriEmpty(); + error BaseUriTooLong(); + error InvalidMsgValue(); + + event Minted(uint256 indexed tokenId, address indexed receiver); + + //constructor for the logic(!) contract + constructor( + IWormhole wormhole, + IERC20 dustToken, + bytes32 emitterAddress, + bytes memory baseUri + ) { + if (baseUri.length == 0) { + revert BaseUriEmpty(); + } + if (baseUri.length > 32) { + revert BaseUriTooLong(); + } + + _wormhole = wormhole; + _dustToken = dustToken; + _emitterAddress = emitterAddress; + _baseUri = bytes32(baseUri); + _baseUriLength = uint8(baseUri.length); + + //brick logic contract + initialize("", "", 0, 0, address(1), 0); + renounceOwnership(); + } + + //intentionally empty (we only want the onlyOwner modifier "side-effect") + function _authorizeUpgrade(address) internal override onlyOwner {} + + //"constructor" of the proxy contract + function initialize( + string memory name, + string memory symbol, + uint256 dustAmountOnMint, + uint256 gasTokenAmountOnMint, + address royaltyReceiver, + uint96 royaltyFeeNumerator + ) public initializer { + _dustAmountOnMint = dustAmountOnMint; + _gasTokenAmountOnMint = gasTokenAmountOnMint; + __UUPSUpgradeable_init(); + __ERC721_init(name, symbol); + __ERC2981_init(); + __Ownable_init(); + + _setDefaultRoyalty(royaltyReceiver, royaltyFeeNumerator); + } + + function updateAmountsOnMint( + uint256 dustAmountOnMint, + uint256 gasTokenAmountOnMint + ) external onlyOwner { + _dustAmountOnMint = dustAmountOnMint; + _gasTokenAmountOnMint = gasTokenAmountOnMint; + } + + function getAmountsOnMint() + external + view + returns (uint256 dustAmountOnMint, uint256 gasTokenAmountOnMint) + { + dustAmountOnMint = _dustAmountOnMint; + gasTokenAmountOnMint = _gasTokenAmountOnMint; + } + + /** + * Mints an NFT based on an valid VAA and kickstarts the recipient's wallet with + * gas tokens (ETH or MATIC) and DUST (taken from msg.sender unless msg.sender is recipient). + * TokenId and recipient address are taken from the VAA. + * The Wormhole message must have been published by the DeBridge instance of the + * NFT collection with the specified emitter on Solana (chainId = 1). + */ + function receiveAndMint(bytes calldata vaa) external payable { + (IWormhole.VM memory vm, bool valid, string memory reason) = _wormhole.parseAndVerifyVM( + vaa + ); + if (!valid) revert FailedVaaParseAndVerification(reason); + + if (vm.emitterChainId != SOURCE_CHAIN_ID) revert WrongEmitterChainId(); + + if (vm.emitterAddress != _emitterAddress) revert WrongEmitterAddress(); + + if (_claimedVaas[vm.hash]) revert VaaAlreadyClaimed(); + + _claimedVaas[vm.hash] = true; + + (uint256 tokenId, address evmRecipient) = parsePayload(vm.payload); + _safeMint(evmRecipient, tokenId); + emit Minted(tokenId, evmRecipient); + + if (msg.sender != evmRecipient) { + if (msg.value != _gasTokenAmountOnMint) revert InvalidMsgValue(); + + payable(evmRecipient).transfer(msg.value); + _dustToken.safeTransferFrom(msg.sender, evmRecipient, _dustAmountOnMint); + } + //if the recipient relays the message themselves then they must not include any gas token + else if (msg.value != 0) revert InvalidMsgValue(); + } + + function parsePayload( + bytes memory message + ) internal pure returns (uint256 tokenId, address evmRecipient) { + if (message.length != BytesLib.uint16Size + BytesLib.addressSize) + revert InvalidMessageLength(); + + tokenId = message.toUint16(0); + evmRecipient = message.toAddress(BytesLib.uint16Size); + } + + // ---- ERC721 ---- + + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + return super.tokenURI(tokenId); + } + + function _baseURI() internal view virtual override returns (string memory baseUri) { + baseUri = new string(_baseUriLength); + bytes32 tmp = _baseUri; + assembly { + mstore(add(baseUri, 32), tmp) + } + } + + // ---- ERC2981 ---- + + function setDefaultRoyalty(address receiver, uint96 feeNumerator) external onlyOwner { + _setDefaultRoyalty(receiver, feeNumerator); + } + + function deleteDefaultRoyalty() external onlyOwner { + _deleteDefaultRoyalty(); + } + + function setTokenRoyalty( + uint256 tokenId, + address receiver, + uint96 feeNumerator + ) external onlyOwner { + _setTokenRoyalty(tokenId, receiver, feeNumerator); + } + + function resetTokenRoyalty(uint256 tokenId) external onlyOwner { + _resetTokenRoyalty(tokenId); + } + + // ---- ERC165 ---- + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC721Upgradeable, ERC2981Upgradeable) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/contracts/dependencies/degods-staking/DeGods.sol b/contracts/dependencies/degods-staking/DeGods.sol new file mode 100644 index 000000000..238e358fc --- /dev/null +++ b/contracts/dependencies/degods-staking/DeGods.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {BaseWormholeBridgedNft} from "./BaseWormholeBridgedNft.sol"; +import {IWormhole} from "./wormhole-solidity/IWormhole.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract DeGods is BaseWormholeBridgedNft { + constructor( + IWormhole wormhole, + IERC20 dustToken, + bytes32 emitterAddress, + bytes memory baseUri + ) BaseWormholeBridgedNft(wormhole, dustToken, emitterAddress, baseUri) {} +} diff --git a/contracts/dependencies/degods-staking/DeGodsV2.sol b/contracts/dependencies/degods-staking/DeGodsV2.sol new file mode 100644 index 000000000..1f322e3db --- /dev/null +++ b/contracts/dependencies/degods-staking/DeGodsV2.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {DeGods} from "./DeGods.sol"; +import {BaseWormholeBridgedNft} from "./BaseWormholeBridgedNft.sol"; +import {ERC5058Upgradeable} from "./ERC5058/ERC5058Upgradeable.sol"; +import {IWormhole} from "./wormhole-solidity/IWormhole.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract DeGodsV2 is DeGods, ERC5058Upgradeable { + uint256 private _tokenId; + + constructor( + IWormhole wormhole, + IERC20 dustToken, + bytes32 emitterAddress, + bytes memory baseUri + ) DeGods(wormhole, dustToken, emitterAddress, baseUri) {} + + function symbol() public pure override returns (string memory) { + return "DEGODS"; + } + + function _baseURI() + internal + view + virtual + override(BaseWormholeBridgedNft, ERC721Upgradeable) + returns (string memory) + { + return BaseWormholeBridgedNft._baseURI(); + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId, + uint256 batchSize + ) internal virtual override(ERC721Upgradeable, ERC5058Upgradeable) { + ERC5058Upgradeable._beforeTokenTransfer(from, to, tokenId, batchSize); + } + + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId, + uint256 batchSize + ) internal virtual override(ERC721Upgradeable, ERC5058Upgradeable) { + ERC5058Upgradeable._afterTokenTransfer(from, to, tokenId, batchSize); + } + + function _burn( + uint256 tokenId + ) internal virtual override(ERC721Upgradeable, ERC5058Upgradeable) { + ERC5058Upgradeable._burn(tokenId); + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(BaseWormholeBridgedNft, ERC5058Upgradeable) returns (bool) { + return + ERC5058Upgradeable.supportsInterface(interfaceId) || + BaseWormholeBridgedNft.supportsInterface(interfaceId); + } + + function tokenURI( + uint256 tokenId + ) + public + view + virtual + override(ERC721Upgradeable, BaseWormholeBridgedNft) + returns (string memory) + { + return BaseWormholeBridgedNft.tokenURI(tokenId); + } + + function mint(address to) public virtual { + // We cannot just use balanceOf to create the new tokenId because tokens + // can be burned (destroyed), so we need a separate counter. + _mint(to, _tokenId); + + _tokenId++; + } + + function mint(uint256 count, address to) public virtual { + // We cannot just use balanceOf to create the new tokenId because tokens + // can be burned (destroyed), so we need a separate counter. + for (uint256 index = 0; index < count; index++) { + _mint(to, _tokenId); + + _tokenId++; + } + } +} diff --git a/contracts/dependencies/degods-staking/DummyERC721EnumerableUpgradeable.sol b/contracts/dependencies/degods-staking/DummyERC721EnumerableUpgradeable.sol new file mode 100644 index 000000000..bb33101ad --- /dev/null +++ b/contracts/dependencies/degods-staking/DummyERC721EnumerableUpgradeable.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +// This a dummy implementation to the true ERC721EnumerableUpgradeable that maintains the identical storage layout but +// removes all the ERC721Enumerable methods from the ABI. +// +// IMPORTANT: please make sure the storage layout is identical to ERC721EnumerableUpgradeable!!!!! +abstract contract DummyERC721EnumerableUpgradeable is Initializable, ERC721Upgradeable { + mapping(address => mapping(uint256 => uint256)) internal _ownedTokens; + mapping(uint256 => uint256) internal _ownedTokensIndex; + uint256[] internal _allTokens; + mapping(uint256 => uint256) internal _allTokensIndex; + uint256[46] private __gap; +} diff --git a/contracts/dependencies/degods-staking/ERC5058/ERC5058Upgradeable.sol b/contracts/dependencies/degods-staking/ERC5058/ERC5058Upgradeable.sol new file mode 100644 index 000000000..f03382f79 --- /dev/null +++ b/contracts/dependencies/degods-staking/ERC5058/ERC5058Upgradeable.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.10; + +import {IERC5058Upgradeable} from "./IERC5058Upgradeable.sol"; +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +/** + * @dev Implementation ERC721 Lockable Token + */ +abstract contract ERC5058Upgradeable is ERC721Upgradeable, IERC5058Upgradeable { + // Mapping from token ID to unlock time + mapping(uint256 => uint256) public lockedTokens; + + // Mapping from token ID to lock approved address + mapping(uint256 => address) private _lockApprovals; + + // Mapping from owner to lock operator approvals + mapping(address => mapping(address => bool)) private _lockOperatorApprovals; + + /** + * @dev See {IERC5058-lockApprove}. + */ + function lockApprove(address to, uint256 tokenId) public virtual override { + require(!isLocked(tokenId), "ERC5058: token is locked"); + address owner = ERC721Upgradeable.ownerOf(tokenId); + require(to != owner, "ERC5058: lock approval to current owner"); + + require( + _msgSender() == owner || isLockApprovedForAll(owner, _msgSender()), + "ERC5058: lock approve caller is not owner nor approved for all" + ); + + _lockApprove(owner, to, tokenId); + } + + /** + * @dev See {IERC5058-getLockApproved}. + */ + function getLockApproved(uint256 tokenId) public view virtual override returns (address) { + require(_exists(tokenId), "ERC5058: lock approved query for nonexistent token"); + + return _lockApprovals[tokenId]; + } + + /** + * @dev See {IERC5058-lockerOf}. + */ + function lockerOf(uint256 tokenId) public view virtual override returns (address) { + require(_exists(tokenId), "ERC5058: locker query for nonexistent token"); + require(isLocked(tokenId), "ERC5058: locker query for non-locked token"); + + return _lockApprovals[tokenId]; + } + + /** + * @dev See {IERC5058-setLockApprovalForAll}. + */ + function setLockApprovalForAll(address operator, bool approved) public virtual override { + _setLockApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @dev See {IERC5058-isLockApprovedForAll}. + */ + function isLockApprovedForAll( + address owner, + address operator + ) public view virtual override returns (bool) { + return _lockOperatorApprovals[owner][operator]; + } + + /** + * @dev See {IERC5058-isLocked}. + */ + function isLocked(uint256 tokenId) public view virtual override returns (bool) { + return lockedTokens[tokenId] > block.number; + } + + /** + * @dev See {IERC5058-lockExpiredTime}. + */ + function lockExpiredTime(uint256 tokenId) public view virtual override returns (uint256) { + return lockedTokens[tokenId]; + } + + /** + * @dev See {IERC5058-lock}. + */ + function lock(uint256 tokenId, uint256 expired) public virtual override { + require( + _isLockApprovedOrOwner(_msgSender(), tokenId), + "ERC5058: lock caller is not owner nor approved" + ); + require( + expired > block.number, + "ERC5058: expired time must be greater than current block number" + ); + require(!isLocked(tokenId), "ERC5058: token is locked"); + + _lock(_msgSender(), tokenId, expired); + } + + /** + * @dev See {IERC5058-unlock}. + */ + function unlock(uint256 tokenId) public virtual override { + require(lockerOf(tokenId) == _msgSender(), "ERC5058: unlock caller is not lock operator"); + + address from = ERC721Upgradeable.ownerOf(tokenId); + + _beforeTokenLock(_msgSender(), from, tokenId, 0); + + delete lockedTokens[tokenId]; + + emit Unlocked(_msgSender(), from, tokenId); + + _afterTokenLock(_msgSender(), from, tokenId, 0); + } + + /** + * @dev Locks `tokenId` from `from` until `expired`. + * + * Requirements: + * + * - `tokenId` token must be owned by `from`. + * + * Emits a {Locked} event. + */ + function _lock(address operator, uint256 tokenId, uint256 expired) internal virtual { + address owner = ERC721Upgradeable.ownerOf(tokenId); + + _beforeTokenLock(operator, owner, tokenId, expired); + + lockedTokens[tokenId] = expired; + _lockApprovals[tokenId] = operator; + + emit Locked(operator, owner, tokenId, expired); + + _afterTokenLock(operator, owner, tokenId, expired); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`, but the `tokenId` is locked and cannot be transferred. + * + * Requirements: + * + * - `tokenId` must not exist. + * + * Emits {Locked} and {Transfer} event. + */ + function _safeLockMint( + address to, + uint256 tokenId, + uint256 expired, + bytes memory _data + ) internal virtual { + require(expired > block.number, "ERC5058: lock mint for invalid lock block number"); + + _safeMint(to, tokenId, _data); + + _lock(_msgSender(), tokenId, expired); + } + + /** + * @dev See {ERC721-_burn}. This override additionally clears the lock approvals for the token. + */ + function _burn(uint256 tokenId) internal virtual override { + address owner = ERC721Upgradeable.ownerOf(tokenId); + super._burn(tokenId); + + _beforeTokenLock(_msgSender(), owner, tokenId, 0); + + // clear lock approvals + delete lockedTokens[tokenId]; + delete _lockApprovals[tokenId]; + + _afterTokenLock(_msgSender(), owner, tokenId, 0); + } + + /** + * @dev Approve `to` to lock operate on `tokenId` + * + * Emits a {LockApproval} event. + */ + function _lockApprove(address owner, address to, uint256 tokenId) internal virtual { + _lockApprovals[tokenId] = to; + emit LockApproval(owner, to, tokenId); + } + + /** + * @dev Approve `operator` to lock operate on all of `owner` tokens + * + * Emits a {LockApprovalForAll} event. + */ + function _setLockApprovalForAll( + address owner, + address operator, + bool approved + ) internal virtual { + require(owner != operator, "ERC5058: lock approve to caller"); + _lockOperatorApprovals[owner][operator] = approved; + emit LockApprovalForAll(owner, operator, approved); + } + + /** + * @dev Returns whether `spender` is allowed to lock `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function _isLockApprovedOrOwner( + address spender, + uint256 tokenId + ) internal view virtual returns (bool) { + require(_exists(tokenId), "ERC5058: lock operator query for nonexistent token"); + address owner = ERC721Upgradeable.ownerOf(tokenId); + return (spender == owner || + isLockApprovedForAll(owner, spender) || + getLockApproved(tokenId) == spender); + } + + /** + * @dev See {ERC721-_beforeTokenTransfer}. + * + * Requirements: + * + * - the `tokenId` must not be locked. + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId, + uint256 batchSize + ) internal virtual override { + super._beforeTokenTransfer(from, to, tokenId, batchSize); + + require(!isLocked(tokenId), "ERC5058: token transfer while locked"); + } + + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId, + uint256 batchSize + ) internal virtual override { + super._afterTokenTransfer(from, to, tokenId, batchSize); + // Revoke the lock approval from the previous owner on the current token. + delete _lockApprovals[tokenId]; + } + + /** + * @dev Hook that is called before any token lock/unlock. + * + * Calling conditions: + * + * - `owner` is non-zero. + * - When `expired` is zero, `tokenId` will be unlock for `from`. + * - When `expired` is non-zero, ``from``'s `tokenId` will be locked. + * + */ + function _beforeTokenLock( + address operator, + address owner, + uint256 tokenId, + uint256 expired + ) internal virtual {} + + /** + * @dev Hook that is called after any lock/unlock of tokens. + * + * Calling conditions: + * + * - `owner` is non-zero. + * - When `expired` is zero, `tokenId` will be unlock for `from`. + * - When `expired` is non-zero, ``from``'s `tokenId` will be locked. + * + */ + function _afterTokenLock( + address operator, + address owner, + uint256 tokenId, + uint256 expired + ) internal virtual {} + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165Upgradeable, ERC721Upgradeable) returns (bool) { + return + interfaceId == type(IERC5058Upgradeable).interfaceId || + super.supportsInterface(interfaceId); + } + + // @dev This empty reserved space is put in place to allow future versions to add new variables without shifting down + // storage in the inheritance chain. + // See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + uint256[50] private __gap; +} diff --git a/contracts/dependencies/degods-staking/ERC5058/IERC5058Upgradeable.sol b/contracts/dependencies/degods-staking/ERC5058/IERC5058Upgradeable.sol new file mode 100644 index 000000000..b24a53140 --- /dev/null +++ b/contracts/dependencies/degods-staking/ERC5058/IERC5058Upgradeable.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.10; + +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +/** + * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension + * ERC721 Token that can be locked for a certain period and cannot be transferred. + * This is designed for a non-escrow staking contract that comes later to lock a user's NFT + * while still letting them keep it in their wallet. + * This extension can ensure the security of user tokens during the staking period. + * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT + * airdrop can be avoided, because the airdrop is still in the user's wallet + */ + +interface IERC5058Upgradeable is IERC721Upgradeable { + /** + * @dev Emitted when `tokenId` token is locked by `operator` from `owner`. + */ + event Locked( + address indexed operator, + address indexed owner, + uint256 indexed tokenId, + uint256 expired + ); + + /** + * @dev Emitted when `tokenId` token is unlocked by `operator` from `owner`. + */ + event Unlocked(address indexed operator, address indexed owner, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to lock the `tokenId` token. + */ + event LockApproval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to lock all of its tokens. + */ + event LockApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the locker who is locking the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function lockerOf(uint256 tokenId) external view returns (address locker); + + /** + * @dev Lock `tokenId` token until the block number is greater than `expired` to be unlocked. + * + * Requirements: + * + * - `tokenId` token must be owned by `owner`. + * - `expired` must be greater than block.number + * - If the caller is not `from`, it must be approved to lock this token + * by either {lockApprove} or {setLockApprovalForAll}. + * + * Emits a {Locked} event. + */ + function lock(uint256 tokenId, uint256 expired) external; + + /** + * @dev Unlock `tokenId` token. + * + * Requirements: + * + * - `tokenId` token must be owned by `from`. + * - the caller must be the operator who locks the token by {lock} + * + * Emits a {Unlocked} event. + */ + function unlock(uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to lock `tokenId` token. + * + * Requirements: + * + * - The caller must own the token or be an approved lock operator. + * - `tokenId` must exist. + * + * Emits an {LockApproval} event. + */ + function lockApprove(address to, uint256 tokenId) external; + + /** + * @dev Approve or remove `operator` as an lock operator for the caller. + * Operators can call {lock} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {LockApprovalForAll} event. + */ + function setLockApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns the account lock approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getLockApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Returns if the `operator` is allowed to lock all of the assets of `owner`. + * + * See {setLockApprovalForAll} + */ + function isLockApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Returns if the `tokenId` token is locked. + */ + function isLocked(uint256 tokenId) external view returns (bool); + + /** + * @dev Returns the `tokenId` token lock expired time. + */ + function lockExpiredTime(uint256 tokenId) external view returns (uint256); +} diff --git a/contracts/dependencies/degods-staking/ERC721PointsStakingV1.sol b/contracts/dependencies/degods-staking/ERC721PointsStakingV1.sol new file mode 100644 index 000000000..4ca2970dc --- /dev/null +++ b/contracts/dependencies/degods-staking/ERC721PointsStakingV1.sol @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import {ERC721HolderUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {EnumerableSetUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; +import {IERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +contract ERC721PointsStakingV1 is +UUPSUpgradeable, +ERC721HolderUpgradeable, +ReentrancyGuardUpgradeable, +Ownable2StepUpgradeable, +PausableUpgradeable +{ + error NoTokenIdsProvided(); + error NotTokenOwner(); + error InvalidManagerAddress(); + error NotManager(); + error PointsInitialized(); + error NotEnoughPoints(); + error TokenIdsPointsLengthMismatch(); + error InvalidStakedTokensArgs(); + error InvalidPointsMultiplier(); + error InitPointsAfterStaking(); + error InvalidInitialPoints(); + error InvalidStakeFee(); + error InvalidUnstakeFee(); + + // All can be fit into a single 256-bit storage slot. + // - 160 + 40 + 16 + 32 = 248 + struct StakingMetadata { + address owner; + uint40 lastUpdated; + uint16 multiplier; + uint32 points; + } + + using SafeERC20Upgradeable for IERC20Upgradeable; + using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet; + /* ========== STATE VARIABLES ========== */ + + IERC721Upgradeable public stakingToken; + + // Same storage slot + IERC20Upgradeable public stakeFeeToken; + uint96 public stakeFee; + + // Same storage slot + IERC20Upgradeable public unstakeFeeToken; + uint96 public unstakeFee; + + // Same storage slot + address public manager; + uint32 public totalStaked; + + mapping(uint256 => StakingMetadata) public stakingMetadata; + mapping(address => EnumerableSetUpgradeable.UintSet) internal _stakedTokens; + + uint16 private constant MIN_POINTS_MULTIPLIER = 100; + uint16 private constant MAX_POINTS_MULTIPLIER = 1000; + uint32 private constant MAX_INITIAL_POINTS = 250000; + uint16 private constant MAX_STAKE_FEE = 100; + uint16 private constant MAX_UNSTAKE_FEE = 100; + + uint96 private maxStakeFeeAmount; + uint96 private maxUnstakeFeeAmount; + + address public feeTreasury; + + /* ========== CONSTRUCTOR ========== */ + + function initialize( + address _stakingToken, + address _stakeFeeToken, + uint256 _stakeFee, + address _unstakeFeeToken, + uint256 _unstakeFee, + address _feeTreasury + ) public initializer { + __UUPSUpgradeable_init(); + __ERC721Holder_init(); + __ReentrancyGuard_init(); + __Ownable2Step_init(); + __Pausable_init(); + + maxStakeFeeAmount = uint96( + MAX_STAKE_FEE * (10 ** IERC20MetadataUpgradeable(_stakeFeeToken).decimals()) + ); + + maxUnstakeFeeAmount = uint96( + MAX_UNSTAKE_FEE * (10 ** IERC20MetadataUpgradeable(_unstakeFeeToken).decimals()) + ); + + feeTreasury = _feeTreasury; + stakingToken = IERC721Upgradeable(_stakingToken); + stakeFeeToken = IERC20Upgradeable(_stakeFeeToken); + unstakeFeeToken = IERC20Upgradeable(_unstakeFeeToken); + _setFees(_stakeFee, _unstakeFee); + } + + /* ========== VIEWS ========== */ + + function getPoints(uint32 _tokenId) external view returns (uint32) { + StakingMetadata memory metadata = stakingMetadata[_tokenId]; + if (metadata.owner == address(0)) { + return metadata.points; + } + + uint16 multiplier = metadata.multiplier == 0 ? 100 : metadata.multiplier; + return + uint32( + metadata.points + ((block.timestamp - metadata.lastUpdated) * multiplier) / 6000 + ); + } + + function numStakedTokens(address owner) external view returns (uint256) { + return _stakedTokens[owner].length(); + } + + function allStakedTokens(address owner) external view returns (uint256[] memory) { + return _stakedTokens[owner].values(); + } + + function stakedTokens( + address owner, + uint256 offset, + uint256 limit + ) external view returns (uint256[] memory) { + EnumerableSetUpgradeable.UintSet storage stakedTokenSet = _stakedTokens[owner]; + uint256 endOffset = offset + limit; + if (endOffset > stakedTokenSet.length()) { + endOffset = stakedTokenSet.length(); + } + if (limit == 0 || endOffset <= offset) { + revert InvalidStakedTokensArgs(); + } + uint256[] memory tokens = new uint256[](endOffset - offset); + for (uint256 i = offset; i < endOffset; ++i) { + tokens[i - offset] = stakedTokenSet.at(i); + } + return tokens; + } + + /* ========== MUTATIVE FUNCTIONS ========== */ + + // @notice Stakes user's NFTs + // @param tokenIds The tokenIds of the NFTs which will be staked + function stake(uint256[] calldata _tokenIds) external nonReentrant whenNotPaused { + if (_tokenIds.length == 0) { + revert NoTokenIdsProvided(); + } + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + // Transfer user's NFTs to the staking contract + _stakeNft(_tokenIds[i]); + // Transfer stake fee to the staking contract + stakeFeeToken.safeTransferFrom(msg.sender, feeTreasury, stakeFee); + StakingMetadata storage metadata = stakingMetadata[_tokenIds[i]]; + // Save who is the staker/depositor of the token + metadata.owner = msg.sender; + // Save the last updated timestamp for the current tokenId + metadata.lastUpdated = uint40(block.timestamp); + } + _updateForStake(_tokenIds); + emit Staked(msg.sender, _tokenIds.length, _tokenIds); + } + + // @notice Withdraws staked user's NFTs + // @param tokenIds The tokenIds of the NFTs which will be withdrawn + function withdraw(uint256[] calldata _tokenIds) external nonReentrant { + if (_tokenIds.length == 0) { + revert NoTokenIdsProvided(); + } + + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + StakingMetadata storage metadata = stakingMetadata[_tokenIds[i]]; + // Check if the user who withdraws is the owner + if (metadata.owner != msg.sender) { + revert NotTokenOwner(); + } + // Transfer NFTs back to the owner + _unstakeNft(_tokenIds[i]); + // Transfer unstake fee to the staking contract + unstakeFeeToken.safeTransferFrom(msg.sender, feeTreasury, unstakeFee); + _refreshPoints(_tokenIds[i]); + // Cleanup the staking metadata for the current tokenId + metadata.owner = address(0); + } + _updateForWithdraw(_tokenIds); + emit Withdrawn(msg.sender, _tokenIds.length, _tokenIds); + } + + function _stakeNft(uint256 tokenId) internal virtual { + stakingToken.safeTransferFrom(msg.sender, address(this), tokenId); + } + + function _unstakeNft(uint256 tokenId) internal virtual { + stakingToken.safeTransferFrom(address(this), msg.sender, tokenId); + } + + function _refreshPoints(uint256 _tokenId) internal { + StakingMetadata storage metadata = stakingMetadata[_tokenId]; + if (metadata.owner != address(0)) { + // If the last updated time is not 0, it means that the tokenId is not new + // and we need to update the points for the current tokenId + uint16 multiplier = metadata.multiplier == 0 ? 100 : metadata.multiplier; + metadata.points += uint32( + ((block.timestamp - metadata.lastUpdated) * multiplier) / 6000 + ); + metadata.lastUpdated = uint40(block.timestamp); + } + } + + function _updateForStake(uint256[] calldata _tokenIds) internal { + EnumerableSetUpgradeable.UintSet storage stakedTokenSet = _stakedTokens[msg.sender]; + totalStaked += uint32(_tokenIds.length); + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + stakedTokenSet.add(_tokenIds[i]); + } + } + + function _updateForWithdraw(uint256[] calldata _tokenIds) internal { + EnumerableSetUpgradeable.UintSet storage stakedTokenSet = _stakedTokens[msg.sender]; + totalStaked -= uint32(_tokenIds.length); + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + stakedTokenSet.remove(_tokenIds[i]); + } + } + + // @notice Spend points (in future upgrades, it will be called by external methods when claiming rewards). + // @param tokenIds The tokenIds of the NFTs which will be spent + function _spendPoints(uint256 _tokenId, uint256 _points) internal { + if (msg.sender != stakingToken.ownerOf(_tokenId)) { + revert NotTokenOwner(); + } + + _refreshPoints(_tokenId); + + StakingMetadata storage metadata = stakingMetadata[_tokenId]; + if (_points > metadata.points) { + revert NotEnoughPoints(); + } + metadata.points -= uint32(_points); + emit SpentPoints(msg.sender, _tokenId, _points); + } + + function _setFees(uint256 _stakeFee, uint256 _unstakeFee) internal { + if (_stakeFee > maxStakeFeeAmount) { + revert InvalidStakeFee(); + } + if (_unstakeFee > maxUnstakeFeeAmount) { + revert InvalidUnstakeFee(); + } + uint96 oldStakeFee = stakeFee; + uint96 oldUnstakeFee = unstakeFee; + + if (_stakeFee != oldStakeFee) { + stakeFee = uint96(_stakeFee); + emit StakeFeeUpdated(address(stakeFeeToken), oldStakeFee, _stakeFee); + } + + if (_unstakeFee != oldUnstakeFee) { + unstakeFee = uint96(_unstakeFee); + emit UnstakeFeeUpdated(address(unstakeFeeToken), oldUnstakeFee, _unstakeFee); + } + } + + /* ========== RESTRICTED OWNER FUNCTIONS ========== */ + + function setManager(address _manager) external onlyOwner { + if (_manager == address(0)) { + revert InvalidManagerAddress(); + } + manager = _manager; + emit ManagerUpdated(_manager); + } + + function unsetManager() external onlyOwner { + manager = address(0); + emit ManagerUpdated(address(0)); + } + + function setFees(uint256 _stakeFee, uint256 _unstakeFee) external onlyOwner { + _setFees(_stakeFee, _unstakeFee); + } + + function setFeeTreasury(address _feeTreasury) external onlyOwner { + address oldFeeTreasury = feeTreasury; + feeTreasury = _feeTreasury; + emit FeeTreasuryUpdated(oldFeeTreasury, _feeTreasury); + } + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + /* ========== RESTRICTED MANAGER FUNCTIONS ========== */ + + function setRewardMultipliers( + uint256[] calldata _tokenIds, + uint256 _newMultiplier + ) external nonReentrant managerOnly { + if (_newMultiplier < MIN_POINTS_MULTIPLIER || _newMultiplier > MAX_POINTS_MULTIPLIER) { + revert InvalidPointsMultiplier(); + } + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + _refreshPoints(_tokenIds[i]); + stakingMetadata[_tokenIds[i]].multiplier = uint16(_newMultiplier); + } + + emit MultiplierUpdated(msg.sender, _newMultiplier, _tokenIds); + } + + // @notice Initialize points + // @param tokenIds The tokenIds of the NFTs to initialize points for + function initPoints( + uint256[] calldata _tokenIds, + uint256[] calldata _points + ) external nonReentrant managerOnly { + if (_tokenIds.length != _points.length) { + revert TokenIdsPointsLengthMismatch(); + } + if (totalStaked > 0) { + revert InitPointsAfterStaking(); + } + for (uint256 i = 0; i < _tokenIds.length; i += 1) { + StakingMetadata storage metadata = stakingMetadata[_tokenIds[i]]; + if (metadata.lastUpdated != 0) { + revert PointsInitialized(); + } + if (_points[i] > MAX_INITIAL_POINTS) { + revert InvalidInitialPoints(); + } + metadata.points = uint32(_points[i]); + metadata.lastUpdated = uint40(block.timestamp); + emit SetPoints(msg.sender, _tokenIds[i], metadata.points); + } + } + + /* ========== MODIFIERS ========== */ + + modifier managerOnly() { + if (msg.sender != manager) { + revert NotManager(); + } + _; + } + + /* ========== EVENTS ========== */ + + event ManagerUpdated(address indexed _manager); + event MultiplierUpdated(address indexed _manager, uint256 _newMultiplier, uint256[] _tokenIds); + event Staked(address indexed _user, uint256 _amount, uint256[] _tokenIds); + event Withdrawn(address indexed _user, uint256 _amount, uint256[] _tokenIds); + event SetPoints(address indexed _manager, uint256 _tokenId, uint256 _points); + event SpentPoints(address indexed _manager, uint256 _tokenId, uint256 _points); + event FeeTreasuryUpdated(address indexed _oldFeeTreasury, address indexed _newFeeTreasury); + event StakeFeeUpdated( + address indexed _feeToken, + uint256 indexed _oldFee, + uint256 indexed _newFee + ); + event UnstakeFeeUpdated( + address indexed _feeToken, + uint256 indexed _oldFee, + uint256 indexed _newFee + ); + + // @dev required by UUPSUpgradeable + function _authorizeUpgrade(address) internal override onlyOwner {} + + // @dev This empty reserved space is put in place to allow future versions to add new variables without shifting down + // storage in the inheritance chain. + // See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + uint256[49] private __gap; +} diff --git a/contracts/dependencies/degods-staking/ERC721PointsStakingV2.sol b/contracts/dependencies/degods-staking/ERC721PointsStakingV2.sol new file mode 100644 index 000000000..eb88f55c3 --- /dev/null +++ b/contracts/dependencies/degods-staking/ERC721PointsStakingV2.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + +import {ERC721PointsStakingV1} from "./ERC721PointsStakingV1.sol"; +import {IERC5058Upgradeable} from "./libs/IERC5058Upgradeable.sol"; + +contract ERC721PointsStakingV2 is ERC721PointsStakingV1 { + // Sets expired time to maximum uint256 value so that it's essentially never automatically expired. + uint256 private constant MAX_EXPIRE_TIME = + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + function _stakeNft(uint256 tokenId) internal virtual override { + // This special check is needed as otherwise a user would be able to stake an un-owned token whose owner has + // previously approved staking contract for locking all its tokens. + if (stakingToken.ownerOf(tokenId) != msg.sender) { + revert NotTokenOwner(); + } + IERC5058Upgradeable(address(stakingToken)).lock(tokenId, MAX_EXPIRE_TIME); + } + + function _unstakeNft(uint256 tokenId) internal virtual override { + if (stakingToken.ownerOf(tokenId) == address(this)) { + // For custodial-staked tokens deposited before the non-custodial upgrade, transfer back the token to the owner. + stakingToken.safeTransferFrom(address(this), msg.sender, tokenId); + } else { + // For non-custodial staked tokens, unlock the token. + IERC5058Upgradeable(address(stakingToken)).unlock(tokenId); + } + } + + // @dev This empty reserved space is put in place to allow future versions to add new variables without shifting down + // storage in the inheritance chain. + // See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + uint256[50] private __gap; +} diff --git a/contracts/dependencies/degods-staking/libs/IERC5058Upgradeable.sol b/contracts/dependencies/degods-staking/libs/IERC5058Upgradeable.sol new file mode 100644 index 000000000..b24a53140 --- /dev/null +++ b/contracts/dependencies/degods-staking/libs/IERC5058Upgradeable.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.10; + +import {IERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +/** + * @dev ERC-721 Non-Fungible Token Standard, optional lockable extension + * ERC721 Token that can be locked for a certain period and cannot be transferred. + * This is designed for a non-escrow staking contract that comes later to lock a user's NFT + * while still letting them keep it in their wallet. + * This extension can ensure the security of user tokens during the staking period. + * If the nft lending protocol is compatible with this extension, the trouble caused by the NFT + * airdrop can be avoided, because the airdrop is still in the user's wallet + */ + +interface IERC5058Upgradeable is IERC721Upgradeable { + /** + * @dev Emitted when `tokenId` token is locked by `operator` from `owner`. + */ + event Locked( + address indexed operator, + address indexed owner, + uint256 indexed tokenId, + uint256 expired + ); + + /** + * @dev Emitted when `tokenId` token is unlocked by `operator` from `owner`. + */ + event Unlocked(address indexed operator, address indexed owner, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables `approved` to lock the `tokenId` token. + */ + event LockApproval(address indexed owner, address indexed approved, uint256 indexed tokenId); + + /** + * @dev Emitted when `owner` enables or disables (`approved`) `operator` to lock all of its tokens. + */ + event LockApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /** + * @dev Returns the locker who is locking the `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function lockerOf(uint256 tokenId) external view returns (address locker); + + /** + * @dev Lock `tokenId` token until the block number is greater than `expired` to be unlocked. + * + * Requirements: + * + * - `tokenId` token must be owned by `owner`. + * - `expired` must be greater than block.number + * - If the caller is not `from`, it must be approved to lock this token + * by either {lockApprove} or {setLockApprovalForAll}. + * + * Emits a {Locked} event. + */ + function lock(uint256 tokenId, uint256 expired) external; + + /** + * @dev Unlock `tokenId` token. + * + * Requirements: + * + * - `tokenId` token must be owned by `from`. + * - the caller must be the operator who locks the token by {lock} + * + * Emits a {Unlocked} event. + */ + function unlock(uint256 tokenId) external; + + /** + * @dev Gives permission to `to` to lock `tokenId` token. + * + * Requirements: + * + * - The caller must own the token or be an approved lock operator. + * - `tokenId` must exist. + * + * Emits an {LockApproval} event. + */ + function lockApprove(address to, uint256 tokenId) external; + + /** + * @dev Approve or remove `operator` as an lock operator for the caller. + * Operators can call {lock} for any token owned by the caller. + * + * Requirements: + * + * - The `operator` cannot be the caller. + * + * Emits an {LockApprovalForAll} event. + */ + function setLockApprovalForAll(address operator, bool approved) external; + + /** + * @dev Returns the account lock approved for `tokenId` token. + * + * Requirements: + * + * - `tokenId` must exist. + */ + function getLockApproved(uint256 tokenId) external view returns (address operator); + + /** + * @dev Returns if the `operator` is allowed to lock all of the assets of `owner`. + * + * See {setLockApprovalForAll} + */ + function isLockApprovedForAll(address owner, address operator) external view returns (bool); + + /** + * @dev Returns if the `tokenId` token is locked. + */ + function isLocked(uint256 tokenId) external view returns (bool); + + /** + * @dev Returns the `tokenId` token lock expired time. + */ + function lockExpiredTime(uint256 tokenId) external view returns (uint256); +} diff --git a/contracts/dependencies/degods-staking/wormhole-solidity/BytesLib.sol b/contracts/dependencies/degods-staking/wormhole-solidity/BytesLib.sol new file mode 100644 index 000000000..b557ff4c7 --- /dev/null +++ b/contracts/dependencies/degods-staking/wormhole-solidity/BytesLib.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Unlicense +/* + * @title Solidity Bytes Arrays Utils + * @author Gonçalo Sá + * + * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. + * This is a reduced version of the library. + */ +pragma solidity >=0.8.0 <0.9.0; + +library BytesLib { + uint256 private constant freeMemoryPtr = 0x40; + uint256 private constant maskModulo32 = 0x1f; + /** + * Size of word read by `mload` instruction. + */ + uint256 private constant memoryWord = 32; + uint256 internal constant uint8Size = 1; + uint256 internal constant uint16Size = 2; + uint256 internal constant uint32Size = 4; + uint256 internal constant uint64Size = 8; + uint256 internal constant uint128Size = 16; + uint256 internal constant uint256Size = 32; + uint256 internal constant addressSize = 20; + /** + * Bits in 12 bytes. + */ + uint256 private constant bytes12Bits = 96; + + function slice(bytes memory buffer, uint256 startIndex, uint256 length) internal pure returns (bytes memory) { + unchecked { + require(length + 31 >= length, "slice_overflow"); + } + require(buffer.length >= startIndex + length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(freeMemoryPtr) + + switch iszero(length) + case 0 { + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(length, maskModulo32) + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let startOffset := add(lengthmod, mul(memoryWord, iszero(lengthmod))) + + let dst := add(tempBytes, startOffset) + let end := add(dst, length) + + for { let src := add(add(buffer, startOffset), startIndex) } lt(dst, end) { + dst := add(dst, memoryWord) + src := add(src, memoryWord) + } { mstore(dst, mload(src)) } + + // Update free-memory pointer + // allocating the array padded to 32 bytes like the compiler does now + // Note that negating bitwise the `maskModulo32` produces a mask that aligns addressing to 32 bytes. + mstore(freeMemoryPtr, and(add(dst, maskModulo32), not(maskModulo32))) + } + //if we want a zero-length slice let's just return a zero-length array + default { mstore(freeMemoryPtr, add(tempBytes, memoryWord)) } + + // Store the length of the buffer + // We need to do it even if the length is zero because Solidity does not garbage collect + mstore(tempBytes, length) + } + + return tempBytes; + } + + function toAddress(bytes memory buffer, uint256 startIndex) internal pure returns (address) { + require(buffer.length >= startIndex + addressSize, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + // We want to shift into the lower 12 bytes and leave the upper 12 bytes clear. + tempAddress := shr(bytes12Bits, mload(add(add(buffer, memoryWord), startIndex))) + } + + return tempAddress; + } + + function toUint8(bytes memory buffer, uint256 startIndex) internal pure returns (uint8) { + require(buffer.length > startIndex, "toUint8_outOfBounds"); + + // Note that `endIndex == startOffset` for a given buffer due to the 32 bytes at the start that store the length. + uint256 startOffset = startIndex + uint8Size; + uint8 tempUint; + assembly { + tempUint := mload(add(buffer, startOffset)) + } + return tempUint; + } + + function toUint16(bytes memory buffer, uint256 startIndex) internal pure returns (uint16) { + uint256 endIndex = startIndex + uint16Size; + require(buffer.length >= endIndex, "toUint16_outOfBounds"); + + uint16 tempUint; + assembly { + // Note that `endIndex == startOffset` for a given buffer due to the 32 bytes at the start that store the length. + tempUint := mload(add(buffer, endIndex)) + } + return tempUint; + } + + function toUint32(bytes memory buffer, uint256 startIndex) internal pure returns (uint32) { + uint256 endIndex = startIndex + uint32Size; + require(buffer.length >= endIndex, "toUint32_outOfBounds"); + + uint32 tempUint; + assembly { + // Note that `endIndex == startOffset` for a given buffer due to the 32 bytes at the start that store the length. + tempUint := mload(add(buffer, endIndex)) + } + return tempUint; + } + + function toUint64(bytes memory buffer, uint256 startIndex) internal pure returns (uint64) { + uint256 endIndex = startIndex + uint64Size; + require(buffer.length >= endIndex, "toUint64_outOfBounds"); + + uint64 tempUint; + assembly { + // Note that `endIndex == startOffset` for a given buffer due to the 32 bytes at the start that store the length. + tempUint := mload(add(buffer, endIndex)) + } + return tempUint; + } + + function toUint128(bytes memory buffer, uint256 startIndex) internal pure returns (uint128) { + uint256 endIndex = startIndex + uint128Size; + require(buffer.length >= endIndex, "toUint128_outOfBounds"); + + uint128 tempUint; + assembly { + // Note that `endIndex == startOffset` for a given buffer due to the 32 bytes at the start that store the length. + tempUint := mload(add(buffer, endIndex)) + } + return tempUint; + } + + function toUint256(bytes memory buffer, uint256 startIndex) internal pure returns (uint256) { + uint256 endIndex = startIndex + uint256Size; + require(buffer.length >= endIndex, "toUint256_outOfBounds"); + + uint256 tempUint; + assembly { + // Note that `endIndex == startOffset` for a given buffer due to the 32 bytes at the start that store the length. + tempUint := mload(add(buffer, endIndex)) + } + return tempUint; + } + + function toBytes32(bytes memory buffer, uint256 startIndex) internal pure returns (bytes32) { + uint256 endIndex = startIndex + uint256Size; + require(buffer.length >= endIndex, "toBytes32_outOfBounds"); + + bytes32 tempBytes32; + assembly { + // Note that `endIndex == startOffset` for a given buffer due to the 32 bytes at the start that store the length. + tempBytes32 := mload(add(buffer, endIndex)) + } + return tempBytes32; + } +} diff --git a/contracts/dependencies/degods-staking/wormhole-solidity/IWormhole.sol b/contracts/dependencies/degods-staking/wormhole-solidity/IWormhole.sol new file mode 100644 index 000000000..aa16b562d --- /dev/null +++ b/contracts/dependencies/degods-staking/wormhole-solidity/IWormhole.sol @@ -0,0 +1,148 @@ +// contracts/Messages.sol +// SPDX-License-Identifier: Apache 2 + +pragma solidity ^0.8.0; + +interface IWormhole { + struct GuardianSet { + address[] keys; + uint32 expirationTime; + } + + struct Signature { + bytes32 r; + bytes32 s; + uint8 v; + uint8 guardianIndex; + } + + struct VM { + uint8 version; + uint32 timestamp; + uint32 nonce; + uint16 emitterChainId; + bytes32 emitterAddress; + uint64 sequence; + uint8 consistencyLevel; + bytes payload; + uint32 guardianSetIndex; + Signature[] signatures; + bytes32 hash; + } + + struct ContractUpgrade { + bytes32 module; + uint8 action; + uint16 chain; + address newContract; + } + + struct GuardianSetUpgrade { + bytes32 module; + uint8 action; + uint16 chain; + GuardianSet newGuardianSet; + uint32 newGuardianSetIndex; + } + + struct SetMessageFee { + bytes32 module; + uint8 action; + uint16 chain; + uint256 messageFee; + } + + struct TransferFees { + bytes32 module; + uint8 action; + uint16 chain; + uint256 amount; + bytes32 recipient; + } + + struct RecoverChainId { + bytes32 module; + uint8 action; + uint256 evmChainId; + uint16 newChainId; + } + + event LogMessagePublished( + address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel + ); + event ContractUpgraded(address indexed oldContract, address indexed newContract); + event GuardianSetAdded(uint32 indexed index); + + function publishMessage(uint32 nonce, bytes memory payload, uint8 consistencyLevel) + external + payable + returns (uint64 sequence); + + function initialize() external; + + function parseAndVerifyVM(bytes calldata encodedVM) + external + view + returns (VM memory vm, bool valid, string memory reason); + + function verifyVM(VM memory vm) external view returns (bool valid, string memory reason); + + function verifySignatures(bytes32 hash, Signature[] memory signatures, GuardianSet memory guardianSet) + external + pure + returns (bool valid, string memory reason); + + function parseVM(bytes memory encodedVM) external pure returns (VM memory vm); + + function quorum(uint256 numGuardians) external pure returns (uint256 numSignaturesRequiredForQuorum); + + function getGuardianSet(uint32 index) external view returns (GuardianSet memory); + + function getCurrentGuardianSetIndex() external view returns (uint32); + + function getGuardianSetExpiry() external view returns (uint32); + + function governanceActionIsConsumed(bytes32 hash) external view returns (bool); + + function isInitialized(address impl) external view returns (bool); + + function chainId() external view returns (uint16); + + function isFork() external view returns (bool); + + function governanceChainId() external view returns (uint16); + + function governanceContract() external view returns (bytes32); + + function messageFee() external view returns (uint256); + + function evmChainId() external view returns (uint256); + + function nextSequence(address emitter) external view returns (uint64); + + function parseContractUpgrade(bytes memory encodedUpgrade) external pure returns (ContractUpgrade memory cu); + + function parseGuardianSetUpgrade(bytes memory encodedUpgrade) + external + pure + returns (GuardianSetUpgrade memory gsu); + + function parseSetMessageFee(bytes memory encodedSetMessageFee) external pure returns (SetMessageFee memory smf); + + function parseTransferFees(bytes memory encodedTransferFees) external pure returns (TransferFees memory tf); + + function parseRecoverChainId(bytes memory encodedRecoverChainId) + external + pure + returns (RecoverChainId memory rci); + + function submitContractUpgrade(bytes memory _vm) external; + + function submitSetMessageFee(bytes memory _vm) external; + + function submitNewGuardianSet(bytes memory _vm) external; + + function submitTransferFees(bytes memory _vm) external; + + function submitRecoverChainId(bytes memory _vm) external; +} diff --git a/contracts/interfaces/IXTokenType.sol b/contracts/interfaces/IXTokenType.sol index 99addb96b..fc3c50589 100644 --- a/contracts/interfaces/IXTokenType.sol +++ b/contracts/interfaces/IXTokenType.sol @@ -24,7 +24,8 @@ enum XTokenType { PTokenCAPE, NTokenOtherdeed, NTokenStakefish, - NTokenChromieSquiggle + NTokenChromieSquiggle, + NTokenDeGods } interface IXTokenType { diff --git a/contracts/protocol/libraries/helpers/Errors.sol b/contracts/protocol/libraries/helpers/Errors.sol index a4b65827d..fc05dd3f0 100644 --- a/contracts/protocol/libraries/helpers/Errors.sol +++ b/contracts/protocol/libraries/helpers/Errors.sol @@ -133,4 +133,5 @@ library Errors { string public constant CALLER_NOT_OPERATOR = "138"; // The caller of the function is not operator string public constant INVALID_FEE_VALUE = "139"; // invalid fee rate value string public constant TOKEN_NOT_ALLOW_RESCUE = "140"; // token is not allow rescue + string public constant CALLER_NOT_ALLOWED = "141"; // The caller of the function is not allowed } diff --git a/contracts/protocol/tokenization/NToken.sol b/contracts/protocol/tokenization/NToken.sol index 6385d3551..bc06fe341 100644 --- a/contracts/protocol/tokenization/NToken.sol +++ b/contracts/protocol/tokenization/NToken.sol @@ -117,27 +117,11 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { ); if (receiverOfUnderlying != address(this)) { - address underlyingAsset = _ERC721Data.underlyingAsset; - if (timeLockParams.releaseTime != 0) { - ITimeLock timeLock = POOL.TIME_LOCK(); - timeLock.createAgreement( - DataTypes.AssetType.ERC721, - timeLockParams.actionType, - underlyingAsset, - tokenIds, - receiverOfUnderlying, - timeLockParams.releaseTime - ); - receiverOfUnderlying = address(timeLock); - } - - for (uint256 index = 0; index < tokenIds.length; index++) { - IERC721(underlyingAsset).safeTransferFrom( - address(this), - receiverOfUnderlying, - tokenIds[index] - ); - } + _transferUnderlyingTo( + receiverOfUnderlying, + tokenIds, + timeLockParams + ); } return (oldCollateralizedBalance, newCollateralizedBalance); @@ -166,11 +150,19 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { uint256 tokenId, DataTypes.TimeLockParams calldata timeLockParams ) external virtual override onlyPool nonReentrant { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + _transferUnderlyingTo(target, tokenIds, timeLockParams); + } + + function _transferUnderlyingTo( + address target, + uint256[] memory tokenIds, + DataTypes.TimeLockParams calldata timeLockParams + ) internal virtual { address underlyingAsset = _ERC721Data.underlyingAsset; if (timeLockParams.releaseTime != 0) { ITimeLock timeLock = POOL.TIME_LOCK(); - uint256[] memory tokenIds = new uint256[](1); - tokenIds[0] = tokenId; timeLock.createAgreement( DataTypes.AssetType.ERC721, timeLockParams.actionType, @@ -182,11 +174,13 @@ contract NToken is VersionedInitializable, MintableIncentivizedERC721, INToken { target = address(timeLock); } - IERC721(underlyingAsset).safeTransferFrom( - address(this), - target, - tokenId - ); + for (uint256 index = 0; index < tokenIds.length; index++) { + IERC721(underlyingAsset).safeTransferFrom( + address(this), + target, + tokenIds[index] + ); + } } /** diff --git a/contracts/protocol/tokenization/NTokenDeGods.sol b/contracts/protocol/tokenization/NTokenDeGods.sol new file mode 100644 index 000000000..1d96cfa5f --- /dev/null +++ b/contracts/protocol/tokenization/NTokenDeGods.sol @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.10; + +import {NToken} from "./NToken.sol"; +import {IPool} from "../../interfaces/IPool.sol"; +import {IERC20} from "../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC721} from "../../dependencies/openzeppelin/contracts/IERC721.sol"; +import {SafeERC20} from "../../dependencies/openzeppelin/contracts/SafeERC20.sol"; +import {Errors} from "../libraries/helpers/Errors.sol"; +import {XTokenType} from "../../interfaces/IXTokenType.sol"; +import {ERC721PointsStakingV2} from "../../dependencies/degods-staking/ERC721PointsStakingV2.sol"; +import {IERC5058Upgradeable} from "../../dependencies/degods-staking/ERC5058/IERC5058Upgradeable.sol"; +import {INToken} from "../../interfaces/INToken.sol"; +import {IRewardController} from "../../interfaces/IRewardController.sol"; +import {DataTypes} from "../libraries/types/DataTypes.sol"; +import {IACLManager} from "../../interfaces/IACLManager.sol"; + +/** + * @title NTokenBAKC + * + * @notice Implementation of the NTokenBAKC for the ParaSpace protocol + */ +contract NTokenDeGods is NToken { + using SafeERC20 for IERC20; + + ERC721PointsStakingV2 private immutable deGodsStaking; + //we recording fee tokens as immutable here, because stakeFeeToken and unstakeFeeToken can't be updated in deGodsStaking contract + //while fetching fee dynamically since fee can be updated by owner at any time + IERC20 private immutable stakeFeeToken; + + /** + * @dev Constructor. + * @param pool The address of the Pool contract + */ + constructor( + IPool pool, + address delegateRegistry, + address _deGodsStaking + ) NToken(pool, false, delegateRegistry) { + deGodsStaking = ERC721PointsStakingV2(_deGodsStaking); + stakeFeeToken = IERC20(address(deGodsStaking.stakeFeeToken())); + } + + function initialize( + IPool initializingPool, + address underlyingAsset, + IRewardController incentivesController, + string calldata nTokenName, + string calldata nTokenSymbol, + bytes calldata params + ) public virtual override initializer { + super.initialize( + initializingPool, + underlyingAsset, + incentivesController, + nTokenName, + nTokenSymbol, + params + ); + + //approve fee token for staking contract + uint256 allowance = stakeFeeToken.allowance( + address(this), + address(deGodsStaking) + ); + if (allowance == 0) { + stakeFeeToken.approve(address(deGodsStaking), type(uint256).max); + } + + bool isLockApproved = IERC5058Upgradeable(underlyingAsset) + .isLockApprovedForAll(address(this), address(deGodsStaking)); + if (!isLockApproved) { + IERC5058Upgradeable(underlyingAsset).setLockApprovalForAll( + address(deGodsStaking), + true + ); + } + } + + function pointStaking(uint256[] memory tokenIds) external { + require(_isPoolAdminOrOwner(tokenIds), Errors.CALLER_NOT_ALLOWED); + uint256 totalFee = _getStakingFee(); + if (totalFee > 0) { + stakeFeeToken.safeTransferFrom( + msg.sender, + address(this), + totalFee * tokenIds.length + ); + } + _pointStaking(tokenIds); + } + + function withdrawFromStaking(uint256[] memory tokenIds) external { + require(_isPoolAdminOrOwner(tokenIds), Errors.CALLER_NOT_ALLOWED); + _withdrawFromStaking(tokenIds); + } + + function _transferUnderlyingTo( + address target, + uint256[] memory tokenIds, + DataTypes.TimeLockParams calldata timeLockParams + ) internal override { + uint256[] memory stakingIds = _findStakingTokenIds(tokenIds); + if (stakingIds.length > 0) { + _withdrawFromStaking(stakingIds); + } + super._transferUnderlyingTo(target, tokenIds, timeLockParams); + } + + function onERC721Received( + address, + address from, + uint256 tokenId, + bytes memory + ) external virtual override returns (bytes4) { + if (msg.sender == _ERC721Data.underlyingAsset) { + uint256 totalFee = _getStakingFee(); + if (totalFee > 0) { + address nTokenOwner = ownerOf(tokenId); + //for normal supplyERC721, nTokenOwner is zero address, payer address is from. + //for any other case, payer address is nTokenOwner + address payer = nTokenOwner != address(0) ? nTokenOwner : from; + uint256 feeBalance = stakeFeeToken.balanceOf(payer); + uint256 feeAllowance = stakeFeeToken.allowance( + payer, + address(this) + ); + if (feeBalance >= totalFee && feeAllowance >= totalFee) { + stakeFeeToken.safeTransferFrom( + payer, + address(this), + totalFee + ); + _singlePointStaking(tokenId); + } + } else { + _singlePointStaking(tokenId); + } + } + + return this.onERC721Received.selector; + } + + function _getStakingFee() internal view returns (uint256) { + uint256 stakingFee = deGodsStaking.stakeFee(); + uint256 unstakingFee = deGodsStaking.unstakeFee(); + return stakingFee + unstakingFee; + } + + function _findStakingTokenIds(uint256[] memory tokenIds) + internal + view + returns (uint256[] memory) + { + uint256 originArrayLength = tokenIds.length; + uint256 newArrayLength = 0; + uint256[] memory newArray = new uint256[](originArrayLength); + for (uint256 index = 0; index < originArrayLength; index++) { + if (_checkIfInStaking(tokenIds[index])) { + newArray[newArrayLength] = tokenIds[index]; + newArrayLength++; + } + } + assembly { + mstore(newArray, newArrayLength) + } + return newArray; + } + + function _checkIfInStaking(uint256 tokenId) internal view returns (bool) { + (address stakingAddress, , , ) = deGodsStaking.stakingMetadata(tokenId); + return stakingAddress == address(this); + } + + function _singlePointStaking(uint256 tokenId) internal { + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + deGodsStaking.stake(tokenIds); + } + + function _pointStaking(uint256[] memory tokenIds) internal { + deGodsStaking.stake(tokenIds); + } + + function _withdrawFromStaking(uint256[] memory tokenIds) internal { + deGodsStaking.withdraw(tokenIds); + } + + function getXTokenType() external pure override returns (XTokenType) { + return XTokenType.NTokenDeGods; + } + + function _isPoolAdminOrOwner(uint256[] memory tokenIds) + internal + view + returns (bool) + { + IACLManager aclManager = IACLManager( + _addressesProvider.getACLManager() + ); + if (!aclManager.isPoolAdmin(msg.sender)) { + uint256 arrayLength = tokenIds.length; + for (uint256 index = 0; index < arrayLength; index++) { + if (ownerOf(tokenIds[index]) != msg.sender) { + return false; + } + } + } + + return true; + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index c7e00a7bc..b1192460f 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -289,6 +289,12 @@ import { PositionMoverLogic__factory, TimeLock, NTokenChromieSquiggle__factory, + NTokenDeGods, + ERC721PointsStakingV2__factory, + NTokenDeGods__factory, + NTokenChromieSquiggle, + DeGodsV2, + DeGodsV2__factory, CLFixedPriceSynchronicityPriceAdapter, CLFixedPriceSynchronicityPriceAdapter__factory, } from "../types"; @@ -334,9 +340,10 @@ import {PoolParametersLibraryAddresses} from "../types/factories/contracts/proto import {PositionMoverLogicLibraryAddresses} from "../types/factories/contracts/protocol/libraries/logic/PositionMoverLogic__factory"; import {pick, upperFirst} from "lodash"; -import {ZERO_ADDRESS} from "./constants"; +import {ONE_ADDRESS, ZERO_ADDRESS} from "./constants"; import {GLOBAL_OVERRIDES} from "./hardhat-constants"; -import {parseEther} from "ethers/lib/utils"; +import {parseEther, solidityKeccak256} from "ethers/lib/utils"; +import {zeroAddress} from "ethereumjs-util"; export const deployPoolAddressesProvider = async ( marketId: string, @@ -1303,6 +1310,7 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { | Meebits | Moonbirds | Contract + | DeGodsV2 | StakefishNFTManager; } = {}; const paraSpaceConfig = getParaSpaceConfig(); @@ -1491,6 +1499,11 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { continue; } + if (tokenSymbol === ERC721TokenContractId.DEGODS) { + tokens[tokenSymbol] = await deployDeGods(verify); + continue; + } + if (tokenSymbol === ERC721TokenContractId.SFVLDR) { const depositContract = await deployDepositContract(verify); const {paraSpaceAdminAddress} = await getParaSpaceAdmins(); @@ -1536,6 +1549,19 @@ export const deployMoonbirds = async ( verify ) as Promise; +export const deployDeGods = async (verify?: boolean) => + withSaveAndVerify( + new DeGodsV2__factory(await getFirstSigner()), + eContractid.DEGODS, + [ + zeroAddress(), + zeroAddress(), + solidityKeccak256(["string"], ["DeGodsTest"]), + DRE.ethers.utils.solidityPack(["uint256"], [10000]), + ], + verify + ) as Promise; + export const deployReservesSetupHelper = async (verify?: boolean) => withSaveAndVerify( new ReservesSetupHelper__factory(await getFirstSigner()), @@ -2303,6 +2329,32 @@ export const deployMockMultiAssetAirdropProject = async ( verify ) as Promise; +export const deployERC721PointsStaking = async (verify?: boolean) => { + const allTokens = await getAllTokens(); + + const pointStaking = await withSaveAndVerify( + new ERC721PointsStakingV2__factory(await getFirstSigner()), + eContractid.ERC721PointsStakingV2, + [], + verify + ); + + const DUST = await deployMintableERC20(["DUST", "DUST", "9"], verify); + + await waitForTx( + await pointStaking.initialize( + allTokens.DEGODS.address, + DUST.address, + "1000000000", + DUST.address, + "3000000000", + ONE_ADDRESS + ) + ); + + return pointStaking; +}; + export const deployApeCoinStaking = async (verify?: boolean) => { const allTokens = await getAllTokens(); const args = [ @@ -3039,7 +3091,32 @@ export const deployChromieSquiggleNTokenImpl = async ( verify, false, libraries - ) as Promise; + ) as Promise; +}; + +export const deployDeGodsNTokenImpl = async ( + poolAddress: tEthereumAddress, + delegationRegistryAddress: tEthereumAddress, + pointStakingAddress: 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 NTokenDeGods__factory(libraries, await getFirstSigner()), + eContractid.NTokenDeGodsImpl, + [poolAddress, delegationRegistryAddress, pointStakingAddress], + verify, + false, + libraries + ) as Promise; }; export const deployStakefishNTokenImpl = async ( diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 60d60be45..17971f60a 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -97,6 +97,9 @@ import { NTokenStakefish__factory, MockLendPool__factory, NTokenChromieSquiggle__factory, + NTokenDeGods__factory, + ERC721PointsStakingV2__factory, + DeGodsV2__factory, } from "../types"; import { getEthersSigners, @@ -593,6 +596,15 @@ export const getMoonBirds = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getDeGods = async (address?: tEthereumAddress) => + await DeGodsV2__factory.connect( + address || + ( + await getDb().get(`${eContractid.DEGODS}.${DRE.network.name}`).value() + ).address, + await getFirstSigner() + ); + export const getNTokenMoonBirds = async (address?: tEthereumAddress) => await NTokenMoonBirds__factory.connect( address || @@ -900,6 +912,17 @@ export const getNTokenBAKC = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getERC721PointsStaking = async (address?: tEthereumAddress) => + await ERC721PointsStakingV2__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.ERC721PointsStakingV2}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + export const getApeCoinStaking = async (address?: tEthereumAddress) => await ApeCoinStaking__factory.connect( address || @@ -1122,6 +1145,17 @@ export const getNTokenChromieSquiggle = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getNTokenDeGods = async (address?: tEthereumAddress) => + await NTokenDeGods__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.NTokenDeGodsImpl}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + export const getHotWalletProxy = async (address?: tEthereumAddress) => await HotWalletProxy__factory.connect( address || diff --git a/helpers/init-helpers.ts b/helpers/init-helpers.ts index 62e9346ca..0bd9feee6 100644 --- a/helpers/init-helpers.ts +++ b/helpers/init-helpers.ts @@ -51,6 +51,8 @@ import { deployChromieSquiggleNTokenImpl, deployAutoYieldApeImplAndAssignItToProxy, deployAutoCompoundApeImplAndAssignItToProxy, + deployDeGodsNTokenImpl, + deployERC721PointsStaking, } from "./contracts-deployments"; import {ZERO_ADDRESS} from "./constants"; @@ -324,6 +326,7 @@ export const initReservesByHelper = async ( eContractid.NTokenMAYCImpl, eContractid.NTokenBAKCImpl, eContractid.NTokenStakefishImpl, + eContractid.NTokenDeGodsImpl, ].includes(xTokenImpl) ) { xTokenType[symbol] = "nft"; @@ -594,6 +597,18 @@ export const initReservesByHelper = async ( verify ) ).address; + } else if (reserveSymbol == ERC721TokenContractId.DEGODS) { + const pointStakingAddress = + (await getContractAddressInDb(eContractid.ERC721PointsStakingV2)) || + (await deployERC721PointsStaking(verify)).address; + xTokenToUse = ( + await deployDeGodsNTokenImpl( + pool.address, + delegationRegistryAddress, + pointStakingAddress, + verify + ) + ).address; } if (!xTokenToUse) { diff --git a/helpers/types.ts b/helpers/types.ts index 20d97c1ae..d07bc592f 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -63,6 +63,7 @@ export enum XTokenType { NTokenOtherdeed = 15, NTokenStakefish = 16, NTokenChromieSquiggle = 17, + NTokenDeGods = 18, } export type ConstructorArgs = ( @@ -238,6 +239,7 @@ export enum eContractid { PoolParametersImpl = "PoolParametersImpl", PoolApeStakingImpl = "PoolApeStakingImpl", ApeCoinStaking = "ApeCoinStaking", + ERC721PointsStakingV2 = "ERC721PointsStakingV2", ATokenDebtToken = "ATokenDebtToken", StETHDebtToken = "StETHDebtToken", CApeDebtToken = "CApeDebtToken", @@ -271,6 +273,7 @@ export enum eContractid { DefaultTimeLockStrategy = "DefaultTimeLockStrategy", NTokenOtherdeedImpl = "NTokenOtherdeedImpl", NTokenChromieSquiggleImpl = "NTokenChromieSquiggleImpl", + NTokenDeGodsImpl = "NTokenDeGodsImpl", NTokenStakefishImpl = "NTokenStakefishImpl", HotWalletProxy = "HotWalletProxy", SFVLDR = "SFVLDR", @@ -411,6 +414,8 @@ export enum ProtocolErrors { INVALID_TOKEN_ID = "135", //invalid token id + CALLER_NOT_ALLOWED = "141", // The caller of the function is not allowed + // SafeCast SAFECAST_UINT128_OVERFLOW = "SafeCast: value doesn't fit in 128 bits", @@ -625,6 +630,7 @@ export enum NTokenContractId { nOTHR = "nOTHR", nSFVLDR = "nSFVLDR", nBLOCKS = "nBLOCKS", + nDEGODS = "nDEGODS", } export enum PTokenContractId { @@ -808,6 +814,10 @@ export interface IStakefish { StakefishManager?: tEthereumAddress; } +export interface IDeGodsStakingConfig { + ERC721PointsStaking?: tEthereumAddress; +} + export interface IOracleConfig { // ParaSpaceOracle BaseCurrency: ERC20TokenContractId | string; @@ -858,6 +868,7 @@ export interface ICommonConfiguration { YogaLabs: IYogaLabs; Uniswap: IUniswapConfig; BendDAO: IBendDAOConfig; + DeGodsStaking: IDeGodsStakingConfig; Stakefish: IStakefish; Marketplace: IMarketplaceConfig; Chainlink: IChainlinkConfig; diff --git a/market-config/index.ts b/market-config/index.ts index 7f7114ea9..402ba0735 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -134,6 +134,7 @@ export const HardhatParaSpaceConfig: IParaSpaceConfiguration = { Marketplace: {}, Chainlink: {}, BendDAO: {}, + DeGodsStaking: {}, Stakefish: {}, // RESERVE ASSETS - CONFIG, ASSETS, BORROW RATES, ReservesConfig: { @@ -168,6 +169,7 @@ export const HardhatParaSpaceConfig: IParaSpaceConfiguration = { SEWER: strategySEWER, PPG: strategyPudgyPenguins, SFVLDR: strategyStakefishValidator, + DEGODS: strategyDEGODS, }, }; @@ -190,6 +192,7 @@ export const MoonbeamParaSpaceConfig: IParaSpaceConfiguration = { Uniswap: {}, Marketplace: {}, BendDAO: {}, + DeGodsStaking: {}, Stakefish: {}, Chainlink: { WGLMR: "0x4497B606be93e773bbA5eaCFCb2ac5E2214220Eb", @@ -245,6 +248,7 @@ export const GoerliParaSpaceConfig: IParaSpaceConfiguration = { LendingPool: "0x84a47EaEca69f8B521C21739224251c8c4566Bbc", LendingPoolLoan: "0x7F64c32a3c13Bd245a7141a607A7E60DA585BA86", }, + DeGodsStaking: {}, Stakefish: { StakefishManager: "0x5b41ffb9c448c02ff3d0401b0374b67efcb73c7e", }, @@ -326,6 +330,7 @@ export const ArbitrumGoerliConfig: IParaSpaceConfiguration = { }, Marketplace: {}, BendDAO: {}, + DeGodsStaking: {}, Stakefish: {}, Chainlink: { WETH: "0x62CAe0FA2da220f43a51F86Db2EDb36DcA9A5A08", @@ -400,6 +405,7 @@ export const ArbitrumOneParaSpaceConfig: IParaSpaceConfiguration = { }, Marketplace: {}, BendDAO: {}, + DeGodsStaking: {}, Stakefish: {}, Chainlink: { WETH: "0x639fe6ab55c921f74e7fac1ee960c0b6293ba612", @@ -519,6 +525,9 @@ export const MainnetParaSpaceConfig: IParaSpaceConfiguration = { LendingPool: "0x70b97a0da65c15dfb0ffa02aee6fa36e507c2762", LendingPoolLoan: "0x5f6ac80CdB9E87f3Cfa6a90E5140B9a16A361d5C", }, + DeGodsStaking: { + ERC721PointsStaking: "0xfa3ce71036dd4564d7d8de19d2b90fb856c5be82", + }, Stakefish: { StakefishManager: "0xffff2d93c83d4c613ed68ca887f057651135e089", }, diff --git a/market-config/reservesConfigs.ts b/market-config/reservesConfigs.ts index 5d3aaa7af..ab671004d 100644 --- a/market-config/reservesConfigs.ts +++ b/market-config/reservesConfigs.ts @@ -553,7 +553,7 @@ export const strategyDEGODS: IReserveParams = { liquidationBonus: "10500", borrowingEnabled: false, reserveDecimals: "0", - xTokenImpl: eContractid.NTokenImpl, + xTokenImpl: eContractid.NTokenDeGodsImpl, reserveFactor: "0", borrowCap: "0", supplyCap: "1000", diff --git a/package.json b/package.json index e75cc1c4c..e0aca8040 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@nomiclabs/hardhat-etherscan": "^3.1.0", "@nomiclabs/hardhat-waffle": "2.0.3", "@openzeppelin/contracts": "4.2.0", - "@openzeppelin/contracts-upgradeable": "4.2.0", + "@openzeppelin/contracts-upgradeable": "4.8.3", "@openzeppelin/hardhat-upgrades": "^1.21.0", "@prb/math": "^2.5.0", "@safe-global/safe-core-sdk": "^3.2.0", diff --git a/scripts/deployments/steps/11_allReserves.ts b/scripts/deployments/steps/11_allReserves.ts index 8e3ac463f..a91dbc8ec 100644 --- a/scripts/deployments/steps/11_allReserves.ts +++ b/scripts/deployments/steps/11_allReserves.ts @@ -95,7 +95,8 @@ export const step_11 = async (verify = false) => { xTokenImpl === eContractid.PTokenCApeImpl || xTokenImpl === eContractid.PYieldTokenImpl || xTokenImpl === eContractid.NTokenBAKCImpl || - xTokenImpl === eContractid.NTokenStakefishImpl + xTokenImpl === eContractid.NTokenStakefishImpl || + xTokenImpl === eContractid.NTokenDeGodsImpl ) as [string, IReserveParams][]; const chunkedReserves = chunk(reserves, 20); diff --git a/scripts/upgrade/ntoken.ts b/scripts/upgrade/ntoken.ts index ae76c097a..2fe9ed1ee 100644 --- a/scripts/upgrade/ntoken.ts +++ b/scripts/upgrade/ntoken.ts @@ -1,6 +1,7 @@ import {getParaSpaceConfig, waitForTx} from "../../helpers/misc-utils"; import { deployChromieSquiggleNTokenImpl, + deployDeGodsNTokenImpl, deployGenericMoonbirdNTokenImpl, deployGenericNTokenImpl, deployNTokenBAKCImpl, @@ -17,6 +18,7 @@ import { getNToken, getApeCoinStaking, getPoolProxy, + getERC721PointsStaking, } from "../../helpers/contracts-getters"; import {NTokenContractId, XTokenType} from "../../helpers/types"; @@ -187,6 +189,17 @@ export const upgradeNToken = async (verify = false) => { verify ) ).address; + } else if (xTokenType == XTokenType.NTokenDeGods) { + console.log("deploy NTokenDeGods implementation"); + const pointStaking = await getERC721PointsStaking(); + newImpl = ( + await deployDeGodsNTokenImpl( + poolAddress, + delegationRegistry, + pointStaking.address, + verify + ) + ).address; } else if (xTokenType == XTokenType.NToken) { // compatibility if (symbol == NTokenContractId.nOTHR) { @@ -217,6 +230,21 @@ export const upgradeNToken = async (verify = false) => { ).address; } newImpl = nTokenBlocksImplementationAddress; + // compatibility + } else if (symbol == NTokenContractId.nDEGODS) { + if (!nTokenBlocksImplementationAddress) { + console.log("deploy NTokenDeGods implementation"); + const pointStaking = await getERC721PointsStaking(); + newImpl = ( + await deployDeGodsNTokenImpl( + poolAddress, + delegationRegistry, + pointStaking.address, + verify + ) + ).address; + } + newImpl = nTokenBlocksImplementationAddress; } else { if (!nTokenImplementationAddress) { console.log("deploy NToken implementation"); diff --git a/test/_pool_core_flash_claim.spec.ts b/test/_pool_core_flash_claim.spec.ts index 8c487381c..cef077cd6 100644 --- a/test/_pool_core_flash_claim.spec.ts +++ b/test/_pool_core_flash_claim.spec.ts @@ -258,7 +258,7 @@ describe("Flash Claim Test", () => { ).to.be.equal(await airdrop_project.erc1155Bonus()); }); - it("TC-flash-claim-03: non-owner can't flash claim airdrop", async function () { + it("TC-flash-claim-04: non-owner can't flash claim airdrop", async function () { const { bayc, users: [user1, user2], @@ -282,7 +282,7 @@ describe("Flash Claim Test", () => { ).to.be.revertedWith("not contract owner"); }); - it("TC-flash-claim-04:user can not flash claim with uniswapV3 [ @skip-on-coverage ]", async function () { + it("TC-flash-claim-05:user can not flash claim with uniswapV3 [ @skip-on-coverage ]", async function () { const { users: [user1], pool, @@ -362,7 +362,7 @@ describe("Flash Claim Test", () => { ).to.be.revertedWith(ProtocolErrors.FLASHCLAIM_NOT_ALLOWED); }); - it("TC-flash-claim-05:user can not flash claim with BAYC or MAYC when sApe is not active or paused[ @skip-on-coverage ]", async function () { + it("TC-flash-claim-06:user can not flash claim with BAYC or MAYC when sApe is not active or paused[ @skip-on-coverage ]", async function () { const { users: [user1], bayc, @@ -405,7 +405,7 @@ describe("Flash Claim Test", () => { ).to.be.revertedWith(ProtocolErrors.RESERVE_INACTIVE); }); - it("TC-flash-claim-06:user can not flash claim when HF < 1", async function () { + it("TC-flash-claim-07:user can not flash claim when HF < 1", async function () { const { users: [user1, depositor], bayc, @@ -441,7 +441,7 @@ describe("Flash Claim Test", () => { ); }); - it("TC-flash-claim-06:user can flash claim with multiple assets", async function () { + it("TC-flash-claim-08:user can flash claim with multiple assets", async function () { const { users: [user1], bayc, @@ -487,7 +487,7 @@ describe("Flash Claim Test", () => { ).to.be.equal(await multi_asset_airdrop_project.erc721Bonus()); }); - it("TC-flash-claim-06:user can flash claim with multiple assets for BAYC Sewer Pass", async function () { + it("TC-flash-claim-09:user can flash claim with multiple assets for BAYC Sewer Pass", async function () { const { users: [user1], bayc, diff --git a/test/xtoken_ntoken_bakc.spec.ts b/test/xtoken_ntoken_bakc.spec.ts index c38c4cf51..ad3bfd6cc 100644 --- a/test/xtoken_ntoken_bakc.spec.ts +++ b/test/xtoken_ntoken_bakc.spec.ts @@ -14,7 +14,7 @@ import { import {parseEther} from "ethers/lib/utils"; import {getAutoCompoundApe} from "../helpers/contracts-getters"; -describe("APE Coin Staking Test", () => { +describe("NToken BAKC Test", () => { let testEnv: TestEnv; const sApeAddress = ONE_ADDRESS; diff --git a/test/xtoken_ntoken_degods.spec.ts b/test/xtoken_ntoken_degods.spec.ts new file mode 100644 index 000000000..8f19b7aa3 --- /dev/null +++ b/test/xtoken_ntoken_degods.spec.ts @@ -0,0 +1,395 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {MAX_UINT_AMOUNT, ONE_ADDRESS} from "../helpers/constants"; +import {waitForTx} from "../helpers/misc-utils"; +import {TestEnv} from "./helpers/make-suite"; +import {testEnvFixture} from "./helpers/setup-env"; +import { + changePriceAndValidate, + mintAndValidate, + supplyAndValidate, +} from "./helpers/validated-steps"; +import { + getAllTokens, + getERC721PointsStaking, + getMintableERC20, + getMintableERC721, + getNTokenDeGods, + getProtocolDataProvider, +} from "../helpers/contracts-getters"; +import { + ERC721PointsStakingV2, + MintableERC20, + MintableERC721, + NTokenDeGods, +} from "../types"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import {parseEther} from "ethers/lib/utils"; +import {ProtocolErrors} from "../helpers/types"; + +describe("NToken DeGods Test", () => { + let testEnv: TestEnv; + let PointStaking: ERC721PointsStakingV2; + let DUST: MintableERC20; + let DeGods: MintableERC721; + let nDeGods: NTokenDeGods; + + const fixture = async () => { + testEnv = await loadFixture(testEnvFixture); + const { + users: [user1], + pool, + poolAdmin, + } = testEnv; + + PointStaking = await getERC721PointsStaking(); + const dustAddress = await PointStaking.stakeFeeToken(); + DUST = await getMintableERC20(dustAddress); + const allTokens = await getAllTokens(); + const protocolDataProvider = await getProtocolDataProvider(); + DeGods = await getMintableERC721(allTokens.DEGODS.address); + const nDeGodsAddress = ( + await protocolDataProvider.getReserveTokensAddresses( + allTokens.DEGODS.address + ) + ).xTokenAddress; + nDeGods = await getNTokenDeGods(nDeGodsAddress); + + await waitForTx( + await DeGods.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + await waitForTx( + await DUST.connect(user1.signer).approve(nDeGods.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await DUST.connect(poolAdmin.signer).approve( + nDeGods.address, + MAX_UINT_AMOUNT + ) + ); + + return testEnv; + }; + + it("user can supply and withdraw degods with staking if approve token transfer", async () => { + const { + users: [user1], + pool, + } = await loadFixture(fixture); + await mintAndValidate(DeGods, "3", user1); + await mintAndValidate(DUST, "12", user1); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + expect(await nDeGods.balanceOf(user1.address)).to.be.equal(0); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + DeGods.address, + [ + {tokenId: 0, useAsCollateral: true}, + {tokenId: 1, useAsCollateral: true}, + {tokenId: 2, useAsCollateral: true}, + ], + user1.address, + "0" + ) + ); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(3); + expect(await nDeGods.balanceOf(user1.address)).to.be.equal(3); + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(DeGods.address, [0, 1, 2], user1.address) + ); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "12") + ); + }); + + it("user can supply and withdraw degods with staking if partial approve token transfer", async () => { + const { + users: [user1], + pool, + } = await loadFixture(fixture); + await mintAndValidate(DeGods, "3", user1); + await mintAndValidate(DUST, "6", user1); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + expect(await nDeGods.balanceOf(user1.address)).to.be.equal(0); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + DeGods.address, + [ + {tokenId: 0, useAsCollateral: true}, + {tokenId: 1, useAsCollateral: true}, + {tokenId: 2, useAsCollateral: true}, + ], + user1.address, + "0" + ) + ); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(1); + expect(await nDeGods.balanceOf(user1.address)).to.be.equal(3); + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(DeGods.address, [0, 1, 2], user1.address) + ); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "4") + ); + }); + + it("user can supply and withdraw degods without staking if didn't approve token transfer", async () => { + const { + users: [user1], + pool, + } = await loadFixture(fixture); + await mintAndValidate(DeGods, "3", user1); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + expect(await nDeGods.balanceOf(user1.address)).to.be.equal(0); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + DeGods.address, + [ + {tokenId: 0, useAsCollateral: true}, + {tokenId: 1, useAsCollateral: true}, + {tokenId: 2, useAsCollateral: true}, + ], + user1.address, + "0" + ) + ); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + expect(await nDeGods.balanceOf(user1.address)).to.be.equal(3); + + await waitForTx( + await pool + .connect(user1.signer) + .withdrawERC721(DeGods.address, [0, 1, 2], user1.address) + ); + + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "0") + ); + }); + + it("ndegods with staking can be liquidated", async () => { + const { + users: [user1, user2], + weth, + pool, + } = await loadFixture(fixture); + await mintAndValidate(DeGods, "1", user1); + await mintAndValidate(DUST, "4", user1); + await supplyAndValidate(weth, "100", user2, true); + + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "0") + ); + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + DeGods.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + "0" + ) + ); + + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "1") + ); + + await changePriceAndValidate(DeGods, "10"); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, parseEther("2"), 0, user1.address) + ); + + await changePriceAndValidate(DeGods, "1"); + + await waitForTx( + await pool + .connect(user2.signer) + .startAuction(user1.address, DeGods.address, 0) + ); + + expect(await DeGods.balanceOf(user2.address)).to.be.equal(0); + + await waitForTx( + await pool + .connect(user2.signer) + .liquidateERC721( + DeGods.address, + user1.address, + 0, + await convertToCurrencyDecimals(weth.address, "100"), + false, + {gasLimit: 5000000, value: parseEther("100")} + ) + ); + expect(await DeGods.balanceOf(user2.address)).to.be.equal(1); + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "4") + ); + }); + + it("ndegods without staking can be liquidated", async () => { + const { + users: [user1, user2], + weth, + pool, + } = await loadFixture(fixture); + await mintAndValidate(DeGods, "1", user1); + await supplyAndValidate(weth, "100", user2, true); + + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "0") + ); + + await waitForTx( + await pool + .connect(user1.signer) + .supplyERC721( + DeGods.address, + [{tokenId: 0, useAsCollateral: true}], + user1.address, + "0" + ) + ); + + await changePriceAndValidate(DeGods, "10"); + + await waitForTx( + await pool + .connect(user1.signer) + .borrow(weth.address, parseEther("2"), 0, user1.address) + ); + + await changePriceAndValidate(DeGods, "1"); + + await waitForTx( + await pool + .connect(user2.signer) + .startAuction(user1.address, DeGods.address, 0) + ); + + await waitForTx( + await pool + .connect(user2.signer) + .liquidateERC721( + DeGods.address, + user1.address, + 0, + await convertToCurrencyDecimals(weth.address, "100"), + false, + {gasLimit: 5000000, value: parseEther("100")} + ) + ); + expect(await DeGods.balanceOf(user2.address)).to.be.equal(1); + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "0") + ); + }); + + it("only ntoken owner or pool admin can staking or withdraw from point staking", async () => { + const { + users: [user1, user2], + pool, + poolAdmin, + } = await loadFixture(fixture); + await mintAndValidate(DeGods, "3", user1); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + DeGods.address, + [ + {tokenId: 0, useAsCollateral: true}, + {tokenId: 1, useAsCollateral: true}, + {tokenId: 2, useAsCollateral: true}, + ], + user1.address, + "0" + ) + ); + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + + await mintAndValidate(DUST, "4", user1); + await mintAndValidate(DUST, "4", poolAdmin); + + await waitForTx(await nDeGods.connect(user1.signer).pointStaking([0])); + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(1); + + await waitForTx(await nDeGods.connect(poolAdmin.signer).pointStaking([1])); + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(2); + + await expect( + nDeGods.connect(user2.signer).pointStaking([2]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_ALLOWED); + + await expect( + nDeGods.connect(user2.signer).withdrawFromStaking([0]) + ).to.be.revertedWith(ProtocolErrors.CALLER_NOT_ALLOWED); + + await waitForTx( + await nDeGods.connect(user1.signer).withdrawFromStaking([0]) + ); + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(1); + + await waitForTx( + await nDeGods.connect(poolAdmin.signer).withdrawFromStaking([1]) + ); + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + }); + + it("check fee for point staking", async () => { + const { + users: [user1], + pool, + } = await loadFixture(fixture); + await mintAndValidate(DeGods, "3", user1); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + DeGods.address, + [ + {tokenId: 0, useAsCollateral: true}, + {tokenId: 1, useAsCollateral: true}, + {tokenId: 2, useAsCollateral: true}, + ], + user1.address, + "0" + ) + ); + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(0); + + await mintAndValidate(DUST, "12", user1); + + await waitForTx( + await nDeGods.connect(user1.signer).pointStaking([0, 1, 2]) + ); + expect(await PointStaking.numStakedTokens(nDeGods.address)).to.be.equal(3); + + expect(await DUST.balanceOf(ONE_ADDRESS)).to.be.equal( + await convertToCurrencyDecimals(DUST.address, "3") + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index fe1765da6..d0e91068b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1335,10 +1335,10 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/contracts-upgradeable@npm:4.2.0": - version: 4.2.0 - resolution: "@openzeppelin/contracts-upgradeable@npm:4.2.0" - checksum: 76f0105f162e3fe96fbb894c7cb9ed5fa1da2b3015a642bd22c6d177ce6795ef3caddee41675cc2c62e5aef900c122b67e561bba31e143c03087b1fe519364e5 +"@openzeppelin/contracts-upgradeable@npm:4.8.3": + version: 4.8.3 + resolution: "@openzeppelin/contracts-upgradeable@npm:4.8.3" + checksum: 022c99bac4828980e771ddf3426a58b5a6f27932a1b1ec93dd2ed3f11c89d8e407867b845bedd0660bdd11c683971d566737c10504db8a1f144f61daec8b5ed6 languageName: node linkType: hard @@ -1413,7 +1413,7 @@ __metadata: "@nomiclabs/hardhat-etherscan": ^3.1.0 "@nomiclabs/hardhat-waffle": 2.0.3 "@openzeppelin/contracts": 4.2.0 - "@openzeppelin/contracts-upgradeable": 4.2.0 + "@openzeppelin/contracts-upgradeable": 4.8.3 "@openzeppelin/hardhat-upgrades": ^1.21.0 "@prb/math": ^2.5.0 "@safe-global/safe-core-sdk": ^3.2.0