diff --git a/.env.example b/.env.example index eec1923..28cc4db 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ PERMIT2="0x000000000022D473030F116dDEE9F6B43aC78BA3" VAULT="0xbA1333333333a1BA1108E8412f11850A5C319bA9" SYMM="0x800822d361335b4d5F352Dac293cA4128b5B605f" USDC="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" -SYMM_LP="0x94Bf449AB92be226109f2Ed3CE2b297Db94bD995" \ No newline at end of file +SYMM_LP="0x94Bf449AB92be226109f2Ed3CE2b297Db94bD995" +FACTORY="0xc06d6a3db75a80b8ae2d56ea8d4baf3509b6ed22" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3673f3d..a46e3dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules -.env +.env* # Hardhat files /cache @@ -19,4 +19,10 @@ ignition/deployments/chain-31337 .idea *.csv package-lock.json -rewards \ No newline at end of file +rewards + +abis +error.log + +scripts/uv.lock +scripts/.venv \ No newline at end of file diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol new file mode 100644 index 0000000..c307e96 --- /dev/null +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @title SymmioBuildersNft + * @notice A simple ERC721 NFT contract for Symmio Builders with brand name customization. + * All complex logic is handled by the SymmioBuildersNftManager contract. + * + * @dev This contract focuses solely on NFT minting, transfers, and brand name management. + * The manager contract handles all lock data, unlock processes, and fee management. + */ + +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "./interfaces/ISymmioBuildersNft.sol"; + +contract SymmioBuildersNft is + Initializable, + ERC721EnumerableUpgradeable, + AccessControlEnumerableUpgradeable, + PausableUpgradeable, + ISymmioBuildersNft +{ + /* ─────────────────────────────── Roles ─────────────────────────────── */ + + /// @notice Role for minting NFTs through the manager contract. + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @notice Role for burning NFTs through the manager contract. + bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + + /// @notice Role for pausing the contract operations. + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// @notice Role for unpausing the contract operations. + bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); + + /* ──────────────────────── Storage Variables ──────────────────────── */ + + /// @notice Counter for generating unique token IDs sequentially. + uint256 private _tokenIdCounter; + + /// @notice Whether transfers are paused. + bool public transfersPaused; + + /// @notice Mapping of token ID to its lock data. + mapping(uint256 => ISymmioBuildersNft.LockData) public lockData; + + /// @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. + uint256[50] private __gap; + + /* ─────────────────────────────── Events ─────────────────────────────── */ + + /** + * @notice Emitted when an NFT is minted. + * @param to Address receiving the NFT. + * @param tokenId ID of the minted NFT. + * @param amount Amount of SYMM tokens locked. + * @param name Name associated with the NFT. + */ + event NFTMinted(address indexed to, uint256 indexed tokenId, uint256 amount, string name); + + /** + * @notice Emitted when an NFT's lock data is updated. + * @param tokenId ID of the NFT. + * @param amount Amount of SYMM tokens locked. + * @param unlockingAmount Amount of SYMM tokens being unlocked. + * @param name Name associated with the NFT. + */ + event LockDataUpdated(uint256 indexed tokenId, uint256 amount, uint256 unlockingAmount, string name); + + /** + * @notice Emitted when the transfer pause state is updated. + * @param paused New pause state (true for paused, false for unpaused). + */ + event TransfersPausedUpdated(bool paused); + + /* ─────────────────────────────── Errors ─────────────────────────────── */ + + error NotTokenOwner(); // caller is not the owner of the NFT + error ZeroAddress(); // zero address provided for critical parameters + error TransfersPaused(); // transfers are paused + error TokenHasActiveUnlock(); // token has an active unlock + + /* ─────────────────────────── Initialization ─────────────────────────── */ + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the upgradeable SymmioBuildersNft contract. + * @param _admin Address to receive admin and all role assignments. + * + * @dev Sets up the ERC721 contract and assigns roles. + */ + function initialize(address _admin) public initializer { + if (_admin == address(0)) revert ZeroAddress(); + + // Initialize parent contracts + __ERC721_init("Symmio Builders NFT", "BUILDERS"); + __ERC721Enumerable_init(); + __AccessControlEnumerable_init(); + __Pausable_init(); + + // Grant roles to admin + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(PAUSER_ROLE, _admin); + _grantRole(UNPAUSER_ROLE, _admin); + } + + /* ────────────────────────── Core Functions ────────────────────────── */ + + /** + * @notice Mint a new NFT with a brand name. + * @param to Address to mint the NFT to. + * @param amount Amount of SYMM tokens to lock. + * @param name Name for the NFT. + * @return tokenId ID of the newly minted NFT. + * + * @dev Only callable by addresses with MINTER_ROLE (typically the manager contract). + */ + function mint(address to, uint256 amount, string memory name) external onlyRole(MINTER_ROLE) whenNotPaused returns (uint256 tokenId) { + tokenId = _tokenIdCounter++; + _safeMint(to, tokenId); + lockData[tokenId] = ISymmioBuildersNft.LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0, name: name }); + + emit NFTMinted(to, tokenId, amount, name); + } + + /** + * @notice Burn an NFT. + * @param tokenId ID of the NFT to burn. + * + * @dev Only callable by addresses with BURNER_ROLE (typically the manager contract). + */ + function burn(uint256 tokenId) external onlyRole(BURNER_ROLE) whenNotPaused { + _burn(tokenId); + delete lockData[tokenId]; + } + + /** + * @notice Update the lock data of an NFT. + * @param tokenId ID of the NFT to update. + * @param amount Amount of SYMM tokens locked. + * @param name Name associated with the NFT. + * + * @dev Only callable by the NFT owner. + */ + function updateLockData(uint256 tokenId, uint256 amount, uint256 unlockingAmount, string memory name) external onlyRole(MINTER_ROLE) { + lockData[tokenId] = ISymmioBuildersNft.LockData({ + amount: amount, + lockTimestamp: lockData[tokenId].lockTimestamp, + unlockingAmount: unlockingAmount, + name: name + }); + + emit LockDataUpdated(tokenId, amount, unlockingAmount, name); + } + + /* ────────────────────────── View Functions ────────────────────────── */ + + /** + * @notice Get the lock data of an NFT. + * @param tokenId ID of the NFT. + * @return Lock data of the NFT. + */ + function getLockData(uint256 tokenId) external view returns (ISymmioBuildersNft.LockData memory) { + return lockData[tokenId]; + } + + /** + * @notice Get the effective locked amount for an NFT (excluding unlocking amounts). + * @param tokenId ID of the NFT. + * @return The effective locked amount available for fee reductions. + */ + function getEffectiveLockedAmount(uint256 tokenId) external view returns (uint256) { + LockData storage data = lockData[tokenId]; + return data.amount - data.unlockingAmount; + } + + /** + * @notice Get all token IDs owned by a user. + * @param user Address of the user. + * @return Array of token IDs owned by the user. + */ + function getUserTokenIds(address user) external view returns (uint256[] memory) { + uint256 balance = balanceOf(user); + uint256[] memory tokenIds = new uint256[](balance); + for (uint256 i = 0; i < balance; i++) { + tokenIds[i] = tokenOfOwnerByIndex(user, i); + } + return tokenIds; + } + + /** + * @notice Get the total effective locked amount for a user across all their NFTs. + * @param user Address of the user. + * @return total Total effective locked amount for fee reduction calculations. + */ + function getUserTotalLocked(address user) external view returns (uint256 total) { + uint256 balance = balanceOf(user); + for (uint256 i = 0; i < balance; i++) { + uint256 tokenId = tokenOfOwnerByIndex(user, i); + LockData storage data = lockData[tokenId]; + total += (data.amount - data.unlockingAmount); + } + } + + /* ───────────────────────── Pause Controls ───────────────────────── */ + + /** + * @notice Pause the contract, disabling minting and transfers. + * @dev Only callable by accounts with PAUSER_ROLE. + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @notice Unpause the contract, enabling minting and transfers. + * @dev Only callable by accounts with UNPAUSER_ROLE. + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } + + /** + * @notice Pause the contract, disabling transfers. + * @dev Only callable by accounts with PAUSER_ROLE. + */ + function pauseTransfers() external onlyRole(PAUSER_ROLE) { + transfersPaused = true; + emit TransfersPausedUpdated(true); + } + + /** + * @notice Unpause the contract, enabling transfers. + * @dev Only callable by accounts with UNPAUSER_ROLE. + */ + function unpauseTransfers() external onlyRole(UNPAUSER_ROLE) { + transfersPaused = false; + emit TransfersPausedUpdated(false); + } + + /* ───────────────────────── Internal Overrides ───────────────────────── */ + + /** + * @dev Override ERC721 update function to enforce transfer restrictions. + * @param to Address to transfer to (address(0) for burns). + * @param tokenId ID of the NFT being updated. + * @param auth Address authorized for the update. + * @return Address of the previous owner. + * + * @dev Prevents transfers if paused or if the NFT has an active unlock process. + * Allows minting (from == address(0)) and burning (to == address(0)). + */ + function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address) { + address from = _ownerOf(tokenId); + + // Allow minting (from == address(0)) and burning (to == address(0)) + // Only restrict actual transfers between addresses + if (from != address(0) && to != address(0)) { + if (transfersPaused) revert TransfersPaused(); + if (lockData[tokenId].unlockingAmount > 0) revert TokenHasActiveUnlock(); + } + + return super._update(to, tokenId, auth); + } + + /* ──────────────────── Interface Support ──────────────────── */ + + /** + * @notice Check if the contract supports a given interface. + * @param interfaceId Interface ID to check. + * @return Whether the interface is supported. + */ + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, IERC165) returns (bool) { + return super.supportsInterface(interfaceId); + } + + /** + * @notice Get the current contract version. + * @return The version string of the current contract. + */ + function version() external pure returns (string memory) { + return "1.0.0"; + } +} diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol new file mode 100644 index 0000000..e6d604b --- /dev/null +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -0,0 +1,668 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @title SymmioBuildersNftManager + * @notice Comprehensive manager contract for SymmioBuildersNft that handles all complex logic + * including SYMM token locking, unlock processes with cliff and vesting, merging, + * fee collection, and cross-chain sync. Integrates full Vesting functionality. + * + * @dev Core features include: + * • SYMM token locking with burning and without burning (for MINTER_ROLE) + * • Lock data management for all NFTs + * • NFT merging functionality + * • Time-locked unlock functionality with cliff periods + * • Full Vesting functionality (linear vesting, penalties, percentage claims) + * • Unlock request management with unique ID tracking + * • Fee collector management and notifications + * • Cross-chain synchronization capabilities + * • Transfer restrictions based on unlock status + * • Token minting capabilities for vesting operations + * + * This contract acts as the central logic hub while the NFT contract remains simple. + */ + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../vesting/VestingV2.sol"; +import "./interfaces/ISymmioBuildersNft.sol"; + +/* ────────────────────────── External Interfaces ────────────────────────── */ + +/// @notice Minimal burnable extension for any ERC‑20 we treat as SYMM. +interface IERC20Burnable is IERC20 { + function burnFrom(address account, uint256 amount) external; +} + +/// @notice Minimal mintable extension for SYMM token. +interface IERC20Mintable is IERC20 { + function mint(address to, uint256 amount) external; +} + +/** + * @notice Interface for the fee collector contract handling fee collection. + */ +interface ISymmioFeeCollector { + function onLockedAmountChanged(int256 amount) external; +} + +contract SymmioBuildersNftManager is VestingV2 { + using SafeERC20 for IERC20; + + /* ─────────────────────────────── Additional Roles ─────────────────────────────── */ + + /// @notice Role for minting NFTs without burning SYMM tokens. + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @notice Role for syncing cross-chain lock data and minting NFTs. + bytes32 public constant SYNC_ROLE = keccak256("SYNC_ROLE"); + + /* ──────────────────────── Storage Variables ──────────────────────── */ + + /// @notice The SYMM token contract address (burnable and mintable). + IERC20Burnable public SYMM; + + /// @notice The SymmioBuildersNft contract. + ISymmioBuildersNft public nftContract; + + /// @notice The minimum amount of SYMM tokens required to mint an NFT. + uint256 public minLockAmount; + + /// @notice Duration of the cliff period in seconds before tokens can be unlocked. + uint256 public cliffDuration; + + /// @notice Duration of the vesting period in seconds after cliff completion. + uint256 public vestingDuration; + + /// @notice Counter for generating unique unlock request IDs sequentially. + uint256 private _unlockIdCounter; + + /// @notice Mapping of token ID to its related fee collector addresses. + mapping(uint256 => address[]) public tokenRelatedFeeCollectors; + + /// @notice Mapping of unlock request ID to complete request details. + mapping(uint256 => UnlockRequest) public unlockRequests; + + /// @notice Mapping of NFT token ID to array of associated unlock request IDs. + mapping(uint256 => uint256[]) public tokenUnlockIds; + + /* ─────────────────────────────── Structs ─────────────────────────────── */ + + /** + * @notice Complete details of an unlock request with status tracking. + * @param amount Amount of tokens to unlock. + * @param unlockInitiatedTime Timestamp when unlock was initiated. + * @param owner Owner of the NFT at unlock initiation. + * @param tokenId ID of the NFT being unlocked. + * @param cliffPassed Whether the cliff period has passed. + * @param vestingStarted Whether vesting has started for this request. + * @param vestingPlanId ID of the created vesting plan in Vesting. + */ + struct UnlockRequest { + uint256 amount; + uint256 unlockInitiatedTime; + address owner; + uint256 tokenId; + bool vestingStarted; + uint256 vestingPlanId; + } + + /* ─────────────────────────────── Events ─────────────────────────────── */ + + /** + * @notice Emitted when an NFT is minted. + * @param to Address receiving the NFT. + * @param tokenId ID of the minted NFT. + * @param amount Amount of SYMM tokens locked. + * @param name Brand name associated with the NFT. + */ + event NFTMinted(address indexed to, uint256 indexed tokenId, uint256 amount, string name); + + /** + * @notice Emitted when SYMM tokens are locked and an NFT is minted. + * @param user Address of the user locking tokens. + * @param tokenId ID of the minted NFT. + * @param amount Amount of SYMM tokens locked. + */ + event TokenLocked(address indexed user, uint256 indexed tokenId, uint256 amount); + + /** + * @notice Emitted when an NFT is minted without burning SYMM. + * @param minter Address of the minter. + * @param to Address receiving the NFT. + * @param tokenId ID of the minted NFT. + * @param amount Amount associated with the NFT. + * @param name Brand name associated with the NFT. + */ + event NFTMintedWithoutBurn(address indexed minter, address indexed to, uint256 indexed tokenId, uint256 amount, string name); + + /** + * @notice Emitted when two NFTs are merged into one. + * @param targetTokenId ID of the NFT receiving the merged amount. + * @param sourceTokenId ID of the NFT being burned. + * @param newAmount New total locked amount in the target NFT. + */ + event TokensMerged(uint256 indexed targetTokenId, uint256 indexed sourceTokenId, uint256 newAmount); + + /** + * @notice Emitted when an unlock process is initiated for an NFT. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens to unlock. + * @param cliffEndTime Timestamp when the cliff period ends. + */ + event UnlockInitiated(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount, uint256 cliffEndTime); + + /** + * @notice Emitted when an unlock process is cancelled. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens cancelled. + */ + event UnlockCancelled(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount); + + /** + * @notice Emitted when vesting starts for an unlock request. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param vestingPlanId ID of the created vesting plan. + * @param owner Owner of the NFT. + * @param amount Amount of tokens entering vesting. + */ + event VestingStarted(uint256 indexed unlockId, uint256 indexed tokenId, address owner, uint256 amount, uint256 vestingPlanId); + + /** + * @notice Emitted when the minimum lock amount is updated. + * @param newMinAmount New minimum lock amount. + */ + event MinLockAmountUpdated(uint256 newMinAmount); + + /** + * @notice Emitted when the cliff duration is updated. + * @param newDuration New cliff duration in seconds. + */ + event CliffDurationUpdated(uint256 newDuration); + + /** + * @notice Emitted when the vesting duration is updated. + * @param newDuration New vesting duration in seconds. + */ + event VestingDurationUpdated(uint256 newDuration); + + /** + * @notice Emitted when fee collectors are added to an NFT. + * @param tokenId ID of the NFT. + * @param feeCollector Address of the fee collector added. + */ + event FeeCollectorAdded(uint256 indexed tokenId, address feeCollector); + + /** + * @notice Emitted when fee collectors are removed from an NFT. + * @param tokenId ID of the NFT. + * @param feeCollector Address of the fee collector removed. + */ + event FeeCollectorRemoved(uint256 indexed tokenId, address feeCollector); + + /* ─────────────────────────────── Errors ─────────────────────────────── */ + + error AmountBelowMinimum(uint256 amount, uint256 minimum); + error NotTokenOwner(); + error InsufficientLockedAmount(); + error InvalidTokenId(); + error InvalidMerge(); + error ZeroAmount(); + error TokenHasActiveUnlock(); + error UnauthorizedAccess(address caller, address requiredCaller); + error LengthMismatch(); + error UnlockNotFound(); + error CliffNotPassed(); + error VestingAlreadyStarted(); + error InvalidDuration(); + + /* ─────────────────────────── Initialization ─────────────────────────── */ + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the SymmioBuildersNftManager contract with full unlock and vesting functionality. + * @param _symm Address of the SYMM token contract. + * @param _nftContract Address of the SymmioBuildersNft contract. + * @param _admin Address to receive admin and all role assignments. + * @param _minLockAmount Minimum amount of SYMM tokens required to mint an NFT. + * @param _cliffDuration Duration of the cliff period in seconds. + * @param _vestingDuration Duration of the vesting period in seconds. + * @param _lockedClaimPenalty Penalty rate for early claims (scaled by 1e18). + * @param _lockedClaimPenaltyReceiver Address to receive penalties from early claims. + */ + function initialize( + address _symm, + address _nftContract, + address _admin, + uint256 _minLockAmount, + uint256 _cliffDuration, + uint256 _vestingDuration, + uint256 _lockedClaimPenalty, + address _lockedClaimPenaltyReceiver + ) public initializer { + if (_symm == address(0) || _nftContract == address(0) || _admin == address(0) || _lockedClaimPenaltyReceiver == address(0)) + revert ZeroAddress(); + if (_cliffDuration == 0 || _vestingDuration == 0 || _lockedClaimPenalty == 0) revert InvalidDuration(); + + // Initialize parent Vesting contract + __vesting_init(_admin, _lockedClaimPenalty, _lockedClaimPenaltyReceiver); + + // Set contract-specific state + SYMM = IERC20Burnable(_symm); + nftContract = ISymmioBuildersNft(_nftContract); + minLockAmount = _minLockAmount; + cliffDuration = _cliffDuration; + vestingDuration = _vestingDuration; + + // Grant additional roles to the admin for initial setup + _grantRole(MINTER_ROLE, _admin); + _grantRole(SETTER_ROLE, _admin); + } + + /* ────────────────────── Core NFT & Locking Functions ────────────────────── */ + + /** + * @notice Mint an NFT by locking SYMM tokens with a custom brand name. + * @param amount Amount of SYMM tokens to lock (must meet minimum requirement). + * @param brandName Custom brand name for the NFT. + * @return tokenId ID of the newly minted NFT. + * + * @dev Burns the SYMM tokens, mints an NFT, stores lock data, and notifies fee collectors. + */ + function mintAndLock(uint256 amount, string memory brandName) external nonReentrant whenNotPaused returns (uint256 tokenId) { + if (amount < minLockAmount) revert AmountBelowMinimum(amount, minLockAmount); + + // Burn the SYMM tokens + SYMM.burnFrom(msg.sender, amount); + + // Mint new NFT + tokenId = nftContract.mint(msg.sender, amount, brandName); + + emit NFTMinted(msg.sender, tokenId, amount, brandName); + } + + /** + * @notice Mint an NFT without burning SYMM tokens (for authorized minters). + * @param to Address to mint the NFT to. + * @param amount Amount to associate with the NFT (must meet minimum requirement). + * @param brandName Custom brand name for the NFT. + * @return tokenId ID of the newly minted NFT. + * + * @dev Only callable by MINTER_ROLE. Creates NFT with tracked amount but no token burn. + */ + function mintWithoutBurn( + address to, + uint256 amount, + string memory brandName + ) external onlyRole(MINTER_ROLE) nonReentrant whenNotPaused returns (uint256 tokenId) { + if (amount < minLockAmount) revert AmountBelowMinimum(amount, minLockAmount); + if (to == address(0)) revert ZeroAddress(); + + // Mint new NFT + tokenId = nftContract.mint(to, amount, brandName); + + emit NFTMintedWithoutBurn(msg.sender, to, tokenId, amount, brandName); + } + + /** + * @notice Lock additional SYMM tokens into an existing NFT. + * @param tokenId ID of the NFT to lock tokens into. + * @param amount Amount of SYMM tokens to lock. + */ + function lock(uint256 tokenId, uint256 amount) external nonReentrant whenNotPaused { + if (amount == 0) revert ZeroAmount(); + + // Burn the SYMM tokens + SYMM.burnFrom(msg.sender, amount); + + // Increase the locked amount + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); + nftContract.updateLockData(tokenId, data.amount + amount, data.unlockingAmount, data.name); + + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); + + emit TokenLocked(msg.sender, tokenId, amount); + } + + /* ────────────────────────── NFT Management ────────────────────────── */ + + /** + * @notice Merge two NFTs owned by the caller into a single NFT. + * @param targetTokenId ID of the NFT to merge into (will receive combined amount). + * @param sourceTokenId ID of the NFT to merge from (will be burned). + */ + function merge(uint256 targetTokenId, uint256 sourceTokenId) external nonReentrant whenNotPaused { + if (nftContract.ownerOf(targetTokenId) != msg.sender || nftContract.ownerOf(sourceTokenId) != msg.sender) revert NotTokenOwner(); + if (targetTokenId == sourceTokenId) revert InvalidMerge(); + + ISymmioBuildersNft.LockData memory targetData = nftContract.getLockData(targetTokenId); + ISymmioBuildersNft.LockData memory sourceData = nftContract.getLockData(sourceTokenId); + + if (sourceData.unlockingAmount > 0) revert TokenHasActiveUnlock(); + + // Merge locked amounts + uint256 newAmount = targetData.amount + sourceData.amount; + nftContract.updateLockData(targetTokenId, newAmount, targetData.unlockingAmount, targetData.name); + + // Notify fee collectors for both NFTs + _notifyFeeCollectors(targetTokenId, int256(sourceData.amount)); + _notifyFeeCollectors(sourceTokenId, -int256(sourceData.amount)); + + // Burn the source NFT and clear its data + nftContract.burn(sourceTokenId); + + emit TokensMerged(targetTokenId, sourceTokenId, newAmount); + } + + /* ──────────────────────── Unlock Functions ──────────────────────── */ + + /** + * @notice Initiate the unlock process for a portion of an NFT's locked tokens. + * @param tokenId ID of the NFT to unlock from. + * @param amount Amount of tokens to unlock. + */ + function initiateUnlock(uint256 tokenId, uint256 amount) external nonReentrant whenNotPaused { + if (nftContract.ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); + if (amount == 0) revert ZeroAmount(); + + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); + uint256 availableAmount = data.amount - data.unlockingAmount; + + if (amount > availableAmount) revert InsufficientLockedAmount(); + + // Update the unlocking amount + nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount + amount, data.name); + + // Create unlock request + uint256 unlockId = _unlockIdCounter++; + unlockRequests[unlockId] = UnlockRequest({ + amount: amount, + unlockInitiatedTime: block.timestamp, + owner: msg.sender, + tokenId: tokenId, + vestingStarted: false, + vestingPlanId: 0 + }); + + tokenUnlockIds[tokenId].push(unlockId); + + // Notify fee collectors + _notifyFeeCollectors(tokenId, -int256(amount)); + + emit UnlockInitiated(unlockId, tokenId, msg.sender, amount, block.timestamp + cliffDuration); + } + + /** + * @notice Cancel an unlock request before the cliff period ends. + * @param unlockId ID of the unlock request to cancel. + * + * @dev Removes the unlock request and updates NFT contract. + * Only callable by the NFT owner and only before cliff completion. + */ + function cancelUnlock(uint256 unlockId) external nonReentrant whenNotPaused { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) revert UnlockNotFound(); + if (request.owner != msg.sender) revert NotTokenOwner(); + if (request.vestingStarted) revert VestingAlreadyStarted(); + + uint256 amount = request.amount; + uint256 tokenId = request.tokenId; + address owner = request.owner; + + // Clean up unlock request + delete unlockRequests[unlockId]; + + // Remove unlock ID from token's unlock list + uint256[] storage unlockIds = tokenUnlockIds[tokenId]; + for (uint256 i = 0; i < unlockIds.length; i++) { + if (unlockIds[i] == unlockId) { + unlockIds[i] = unlockIds[unlockIds.length - 1]; + unlockIds.pop(); + break; + } + } + + // Update NFT contract to cancel the unlock + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); + nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount - amount, data.name); + + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); + + emit UnlockCancelled(unlockId, tokenId, owner, amount); + } + + /** + * @notice Complete the cliff period and start vesting for an unlock request. + * @param unlockId ID of the unlock request to process. + * + * @dev Uses inherited Vesting functionality to create a sophisticated vesting plan. + * Only callable by NFT owner after cliff period completion. + */ + function completeCliffAndStartVesting(uint256 unlockId) external nonReentrant whenNotPaused { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) revert UnlockNotFound(); + if (request.owner != msg.sender) revert NotTokenOwner(); + if (request.vestingStarted) revert VestingAlreadyStarted(); + if (block.timestamp < request.unlockInitiatedTime + cliffDuration) revert CliffNotPassed(); + + // Mark vesting as started + request.vestingStarted = true; + + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(request.tokenId); + uint256 newUnlockingAmount = data.unlockingAmount - request.amount; + + // Complete unlock on NFT contract + nftContract.updateLockData(request.tokenId, data.amount, newUnlockingAmount, data.name); + + // Burn the NFT if no locked tokens remain + if (data.amount == 0 && newUnlockingAmount == 0) nftContract.burn(request.tokenId); + + // Create vesting plan using inherited Vesting functionality + address[] memory users = new address[](1); + users[0] = request.owner; + uint256[] memory amounts = new uint256[](1); + amounts[0] = request.amount; + + uint256[] memory planIds = _setupVestingPlans(address(SYMM), block.timestamp, block.timestamp + vestingDuration, users, amounts); + + // Link vesting plan to unlock request + request.vestingPlanId = planIds[0]; + + emit VestingStarted(unlockId, request.tokenId, request.owner, request.amount, planIds[0]); + } + + /* ───────────────────── Cross-Chain Sync Functions ───────────────────── */ + + /** + * @notice Update lock data for multiple NFTs for cross-chain synchronization. + * @param tokenIds Array of token IDs to update. + * @param lockDatas Array of lock data to apply. + */ + function batchUpdateLockData(uint256[] calldata tokenIds, ISymmioBuildersNft.LockData[] calldata lockDatas) external onlyRole(SYNC_ROLE) { + if (tokenIds.length != lockDatas.length) revert LengthMismatch(); + + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 oldAmount = nftContract.getLockData(tokenIds[i]).amount; + uint256 newAmount = lockDatas[i].amount; + nftContract.updateLockData(tokenIds[i], lockDatas[i].amount, lockDatas[i].unlockingAmount, lockDatas[i].name); + + // Notify fee collectors of the change + _notifyFeeCollectors(tokenIds[i], int256(newAmount) - int256(oldAmount)); + } + } + + /* ────────────────────────── Admin Functions ────────────────────────── */ + + /** + * @notice Set the minimum lock amount for minting NFTs. + * @param _minLockAmount New minimum lock amount. + */ + function setMinLockAmount(uint256 _minLockAmount) external onlyRole(SETTER_ROLE) { + if (_minLockAmount == 0) revert ZeroAmount(); + minLockAmount = _minLockAmount; + emit MinLockAmountUpdated(_minLockAmount); + } + + /** + * @notice Update the cliff duration for new unlock requests. + * @param _cliffDuration New cliff duration in seconds. + * + * @dev Only callable by accounts with DURATION_SETTER_ROLE. Must be non-zero. + */ + function setCliffDuration(uint256 _cliffDuration) external onlyRole(SETTER_ROLE) { + if (_cliffDuration == 0) revert InvalidDuration(); + cliffDuration = _cliffDuration; + emit CliffDurationUpdated(_cliffDuration); + } + + /** + * @notice Update the vesting duration for new vesting plans. + * @param _vestingDuration New vesting duration in seconds. + * + * @dev Only callable by accounts with DURATION_SETTER_ROLE. Must be non-zero. + */ + function setVestingDuration(uint256 _vestingDuration) external onlyRole(SETTER_ROLE) { + if (_vestingDuration == 0) revert InvalidDuration(); + vestingDuration = _vestingDuration; + emit VestingDurationUpdated(_vestingDuration); + } + + /** + * @notice Add fee collectors to an NFT. + * @param tokenId ID of the NFT to add fee collectors to. + * @param feeCollectors Array of fee collector addresses to add. + */ + function addFeeCollector(uint256 tokenId, address[] calldata feeCollectors) external onlyRole(SETTER_ROLE) { + for (uint256 i = 0; i < feeCollectors.length; i++) { + tokenRelatedFeeCollectors[tokenId].push(feeCollectors[i]); + emit FeeCollectorAdded(tokenId, feeCollectors[i]); + } + } + + /** + * @notice Remove a fee collector from an NFT. + * @param tokenId ID of the NFT to remove fee collector from. + * @param feeCollector Address of the fee collector to remove. + */ + function removeFeeCollector(uint256 tokenId, address feeCollector) external onlyRole(SETTER_ROLE) { + address[] storage collectors = tokenRelatedFeeCollectors[tokenId]; + for (uint256 i = 0; i < collectors.length; i++) { + if (collectors[i] == feeCollector) { + collectors[i] = collectors[collectors.length - 1]; + collectors.pop(); + break; + } + } + emit FeeCollectorRemoved(tokenId, feeCollector); + } + + /* ────────────────────────── View Functions ────────────────────────── */ + + /** + * @notice Get all fee collectors for a specific NFT. + * @param tokenId ID of the NFT. + * @return Array of fee collector addresses. + */ + function getTokenFeeCollectors(uint256 tokenId) external view returns (address[] memory) { + return tokenRelatedFeeCollectors[tokenId]; + } + + /** + * @notice Get all unlock request IDs for a specific NFT. + * @param tokenId ID of the NFT to query. + * @return Array of unlock request IDs associated with the NFT. + */ + function getTokenUnlockIds(uint256 tokenId) external view returns (uint256[] memory) { + return tokenUnlockIds[tokenId]; + } + + /** + * @notice Get unlock requests for a specific NFT. + * @param tokenId ID of the NFT to query. + * @param start Start index. + * @param end End index. + * @param size Maximum number of requests to return. + * @return Array of unlock requests. + */ + function getUnlockedRequests(uint256 tokenId, uint256 start, uint256 end, uint256 size) external view returns (UnlockRequest[] memory) { + uint256[] memory unlockIds = tokenUnlockIds[tokenId]; + uint256 total = unlockIds.length; + + if (end > total) end = total; + if (start > end) start = end; + + uint256 count = end - start; + if (count > size) count = size; + + UnlockRequest[] memory requests = new UnlockRequest[](count); + for (uint256 i = 0; i < count; i++) requests[i] = unlockRequests[unlockIds[start + i]]; + return requests; + } + + /** + * @notice Get the cliff end time for an unlock request. + * @param unlockId ID of the unlock request. + * @return Timestamp when the cliff period ends, or 0 if request is invalid. + */ + function getCliffEndTime(uint256 unlockId) external view returns (uint256) { + if (unlockId >= _unlockIdCounter) revert UnlockNotFound(); + return unlockRequests[unlockId].unlockInitiatedTime + cliffDuration; + } + + /** + * @notice Check if the cliff period has passed for an unlock request. + * @param unlockId ID of the unlock request. + * @return Whether the cliff period has passed. + */ + function isCliffPassed(uint256 unlockId) external view returns (bool) { + if (unlockId >= _unlockIdCounter) revert UnlockNotFound(); + return block.timestamp >= unlockRequests[unlockId].unlockInitiatedTime + cliffDuration; + } + + /* ───────────────────────── Internal Helpers ───────────────────────── */ + + /** + * @notice Notify all fee collectors for an NFT about locked amount changes. + * @param tokenId ID of the NFT. + * @param amount Change in locked amount (positive or negative). + */ + function _notifyFeeCollectors(uint256 tokenId, int256 amount) private { + address[] storage collectors = tokenRelatedFeeCollectors[tokenId]; + for (uint256 i = 0; i < collectors.length; i++) { + ISymmioFeeCollector(collectors[i]).onLockedAmountChanged(amount); + } + } + + /** + * @notice Override to handle SYMM token minting when needed for vesting. + * @param token Address of the token to mint. + * @param amount Amount of tokens to mint. + * + * @dev This function mints SYMM tokens when the contract needs more tokens for vesting operations. + */ + function _mintTokenIfPossible(address token, uint256 amount) internal virtual override { + if (token == address(SYMM)) { + IERC20Mintable(address(SYMM)).mint(address(this), amount); + } + } + + /** + * @notice Get the current contract version. + * @return The version string of the current contract. + */ + function version() external pure returns (string memory) { + return "1.0.0"; + } +} diff --git a/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol b/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol new file mode 100644 index 0000000..fcf342b --- /dev/null +++ b/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +interface ISymmioBuildersNft is IERC721 { + /** + * @notice Comprehensive lock data structure for each NFT. + * @param amount Total amount of SYMM tokens locked. + * @param lockTimestamp Timestamp when the tokens were locked. + * @param unlockingAmount Amount of tokens currently being unlocked. + */ + struct LockData { + uint256 amount; + uint256 lockTimestamp; + uint256 unlockingAmount; + string name; + } + + function mint(address to, uint256 amount, string memory name) external returns (uint256 tokenId); + + function burn(uint256 tokenId) external; + + function getLockData(uint256 tokenId) external view returns (LockData memory); + + function updateLockData(uint256 tokenId, uint256 amount, uint256 unlockingAmount, string memory name) external; +} diff --git a/contracts/claimSymm/SymmAllocationClaimer.sol b/contracts/claim/SymmAllocationClaimer.sol similarity index 100% rename from contracts/claimSymm/SymmAllocationClaimer.sol rename to contracts/claim/SymmAllocationClaimer.sol diff --git a/contracts/claimSymm/interfaces/IERC20Minter.sol b/contracts/claim/interfaces/IERC20Minter.sol similarity index 100% rename from contracts/claimSymm/interfaces/IERC20Minter.sol rename to contracts/claim/interfaces/IERC20Minter.sol diff --git a/contracts/helpers/ProxyImports.sol b/contracts/helpers/ProxyImports.sol new file mode 100644 index 0000000..3866013 --- /dev/null +++ b/contracts/helpers/ProxyImports.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; diff --git a/contracts/helpers/create2Factory.sol b/contracts/helpers/create2Factory.sol new file mode 100644 index 0000000..ed78b92 --- /dev/null +++ b/contracts/helpers/create2Factory.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +contract Create2Factory { + event Deployed(address addr, uint256 salt); + + constructor() {} + + function deploy(bytes memory bytecode, uint256 salt) public returns (address) { + address addr; + assembly { + addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) + if iszero(extcodesize(addr)) { + revert(0, 0) + } + } + emit Deployed(addr, salt); + return addr; + } + + function getAddress(bytes memory bytecode, uint256 salt) public view returns (address) { + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode))); + return address(uint160(uint256(hash))); + } +} diff --git a/contracts/staking/ISymmStaking.sol b/contracts/staking/ISymmStaking.sol new file mode 100644 index 0000000..5f0a9a1 --- /dev/null +++ b/contracts/staking/ISymmStaking.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.18; + +interface ISymmStaking { + function notifyRewardAmount(address[] calldata tokens, uint256[] calldata amounts) external; +} diff --git a/contracts/staking/RewardNotifier.sol b/contracts/staking/RewardNotifier.sol new file mode 100644 index 0000000..001849b --- /dev/null +++ b/contracts/staking/RewardNotifier.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; + +import { ISymmStaking } from "./ISymmStaking.sol"; + +/** + * @title RewardNotifier + * @notice This contract manages and distributes SYMM token rewards to a staking contract + * @dev Non-upgradeable contract with role-based access control and pause functionality + */ +contract RewardNotifier is AccessControlEnumerable, Pausable { + using SafeERC20 for IERC20; + + // ============ Constants ============ + + /// @notice Role identifier for setting reward parameters + bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); + + /// @notice Role identifier for withdrawing funds + bytes32 public constant WITHDRAWER_ROLE = keccak256("WITHDRAWER_ROLE"); + + /// @notice Role identifier for notifying rewards + bytes32 public constant REWARD_NOTIFIER_ROLE = keccak256("REWARD_NOTIFIER_ROLE"); + + /// @notice Role identifier for pausing the contract + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// @notice Role identifier for unpausing the contract + bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); + + /// @notice Duration of one week in seconds + uint256 public constant WEEK = 1 weeks; + + // ============ State Variables ============ + + /// @notice SYMM token contract + IERC20 public immutable symm; + + /// @notice Staking contract that receives reward notifications + ISymmStaking public immutable staking; + + /// @notice Amount of SYMM tokens to distribute as rewards + uint256 public rewardAmount; + + /// @notice Last reward round (week number) when rewards were distributed + uint256 public lastRewardRound; + + // ============ Errors ============ + + /// @notice Thrown when a zero address is provided + error ZeroAddress(); + + /// @notice Thrown when trying to pay rewards for a week that was already paid + error RewardAlreadyPaid(); + + /// @notice Thrown when reward amount is not set + error RewardAmountNotSet(); + + /// @notice Thrown when an invalid amount is provided + error InvalidAmount(); + + // ============ Events ============ + + /// @notice Emitted when rewards are notified to the staking contract + /// @param round The week number for which rewards were notified + /// @param amount The amount of SYMM tokens distributed + event RewardNotified(uint256 indexed round, uint256 amount); + + /// @notice Emitted when reward amount is updated + /// @param oldAmount Previous reward amount + /// @param newAmount New reward amount + event RewardAmountSet(uint256 oldAmount, uint256 newAmount); + + /// @notice Emitted when funds are rescued from the contract + /// @param token Address of the rescued token + /// @param amount Amount of tokens rescued + /// @param to Address that received the tokens + event FundsRescued(address indexed token, uint256 amount, address indexed to); + + // ============ Constructor ============ + + /** + * @notice Constructs the contract with required addresses + * @param _adminAddress Address that will have DEFAULT_ADMIN_ROLE + * @param _symmAddress Address of the SYMM token contract + * @param _stakingAddress Address of the staking contract + */ + constructor(address _adminAddress, address _symmAddress, address _stakingAddress) { + if (_adminAddress == address(0) || _symmAddress == address(0) || _stakingAddress == address(0)) { + revert ZeroAddress(); + } + + _grantRole(DEFAULT_ADMIN_ROLE, _adminAddress); + staking = ISymmStaking(_stakingAddress); + symm = IERC20(_symmAddress); + } + + // ============ External Functions ============ + + /** + * @notice Notifies the staking contract about available rewards + * @dev Can only be called by REWARD_NOTIFIER_ROLE, once per week, when not paused + * @dev Requires the contract to have sufficient SYMM balance + */ + function notifyReward() external onlyRole(REWARD_NOTIFIER_ROLE) whenNotPaused { + uint256 currentRound = block.timestamp / WEEK; + + if (currentRound <= lastRewardRound) { + revert RewardAlreadyPaid(); + } + + if (rewardAmount == 0) { + revert RewardAmountNotSet(); + } + + // Approve staking contract to spend SYMM tokens + symm.approve(address(staking), rewardAmount); + + // Prepare arrays for notifying rewards + address[] memory tokens = new address[](1); + tokens[0] = address(symm); + + uint256[] memory amounts = new uint256[](1); + amounts[0] = rewardAmount; + + // Notify staking contract + staking.notifyRewardAmount(tokens, amounts); + + // Update last reward round + lastRewardRound = currentRound; + + emit RewardNotified(currentRound, rewardAmount); + } + + /** + * @notice Sets the amount of SYMM tokens to distribute as rewards + * @dev Can only be called by SETTER_ROLE + * @param _amount New reward amount (must be greater than 0) + */ + function setRewardAmount(uint256 _amount) external onlyRole(SETTER_ROLE) { + uint256 oldAmount = rewardAmount; + rewardAmount = _amount; + + emit RewardAmountSet(oldAmount, _amount); + } + + /** + * @notice Pauses the contract, preventing reward notifications + * @dev Can only be called by PAUSER_ROLE + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @notice Unpauses the contract, allowing reward notifications + * @dev Can only be called by UNPAUSER_ROLE + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } + + /** + * @notice Rescues tokens stuck in the contract + * @dev Can only be called by WITHDRAWER_ROLE + * @param token Address of the token to rescue + * @param amount Amount of tokens to rescue + */ + function rescueFunds(address token, uint256 amount) external onlyRole(WITHDRAWER_ROLE) { + IERC20(token).safeTransfer(msg.sender, amount); + + emit FundsRescued(token, amount, msg.sender); + } + + // ============ View Functions ============ + + /** + * @notice Returns the current week number + * @return Current week number since Unix epoch + */ + function getCurrentRound() external view returns (uint256) { + return block.timestamp / WEEK; + } + + /** + * @notice Checks if rewards can be notified for the current week + * @return True if rewards haven't been paid for current week, false otherwise + */ + function canNotifyReward() external view returns (bool) { + return (block.timestamp / WEEK > lastRewardRound) && (rewardAmount > 0) && !paused(); + } +} diff --git a/contracts/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol new file mode 100644 index 0000000..9e2abfa --- /dev/null +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.18; + +import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import { IVesting } from "./interfaces/IVesting.sol"; + +/** + * @title SymmVestingPlanInitializer + * @notice Allows SYMM holders to initialize their individual vesting plans after launch. + * A penalty is applied that increases linearly for every day elapsed after launch. + */ +contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable { + // ============================================================= + // ERRORS + // ============================================================= + + error MismatchedArrays(); + error ZeroAmount(); + error ExceededMaxSymmAmount(uint256 exceededAmount, uint256 maxAllowed); + + // ============================================================= + // ROLES + // ============================================================= + + bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); + + // ============================================================= + // CONSTANTS + // ============================================================= + + uint256 public constant VESTING_DURATION = 140 days; + uint256 public constant PENALTY_PER_DAY_BP = 1e17; // 1% expressed as 0.1 * 1e18 + + // ============================================================= + // IMMUTABLES + // ============================================================= + + uint256 public immutable launchDay; + uint256 public immutable maxVestedSYMM; + address public immutable SYMM; + address public immutable vesting; + + // ============================================================= + // STORAGE + // ============================================================= + + uint256 public pendingTotal; // Total amount yet to be initiated + mapping(address => uint256) public pendingAmount; // Amount a user can still initiate + mapping(address => uint256) public vestedAmount; // Amount a user has already vested + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + constructor(address admin, address _symm, address _vesting, uint256 _maxVestedSYMM, uint256 _launchTimestamp) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(SETTER_ROLE, admin); + _grantRole(PAUSER_ROLE, admin); + _grantRole(UNPAUSER_ROLE, admin); + + SYMM = _symm; + vesting = _vesting; + launchDay = (_launchTimestamp / 1 days) * 1 days; + maxVestedSYMM = _maxVestedSYMM; + } + + // ============================================================= + // ADMIN FUNCTIONS + // ============================================================= + + /** + * @notice Sets the vestable amounts for a list of users. + * @dev Callable only by addresses with SETTER_ROLE. + */ + function setPendingAmounts(address[] calldata users, uint256[] calldata amounts) external onlyRole(SETTER_ROLE) { + if (users.length != amounts.length) revert MismatchedArrays(); + + for (uint256 i; i < users.length; ++i) { + pendingTotal = pendingTotal + amounts[i] - pendingAmount[users[i]]; + if (pendingTotal > maxVestedSYMM) revert ExceededMaxSymmAmount(pendingTotal, maxVestedSYMM); + + pendingAmount[users[i]] = amounts[i]; + } + } + + // ============================================================= + // USER FUNCTIONS + // ============================================================= + + /** + * @notice Starts the vesting plan for the caller. + * @dev Reverts if nothing to vest. Applies penalty based on delay. + */ + function startVesting() external whenNotPaused { + uint256 amount = pendingAmount[msg.sender]; + if (amount == 0) revert ZeroAmount(); + + address[] memory users = new address[](1); + uint256[] memory amounts = new uint256[](1); + users[0] = msg.sender; + amounts[0] = amount; + + IVesting(vesting).setupVestingPlans(SYMM, block.timestamp, _endTime(block.timestamp), users, amounts); + + vestedAmount[msg.sender] += amount; + pendingAmount[msg.sender] = 0; + } + + // ============================================================= + // PAUSE FUNCTIONS + // ============================================================= + + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } + + // ============================================================= + // VIEW FUNCTIONS + // ============================================================= + + /** + * @notice Calculates the end time for new vesting schedules. + * @dev Launch day has weight 0 penalty, full duration. Each day after increases duration linearly. + */ + function endTimeStartsAt(uint256 _timestamp) external view returns (uint256) { + return _endTime(_timestamp); + } + + /** + * @notice Calculates the end time for new vesting schedules. + * @dev Launch day has weight 0 penalty, full duration. Each day after increases duration linearly. + */ + function endTime() external view returns (uint256) { + return _endTime(block.timestamp); + } + + function _endTime(uint256 _timestamp) internal view returns (uint256) { + if(_timestamp >= VESTING_DURATION + launchDay){ + return _timestamp + 14 days; + } + uint256 today = (_timestamp / 1 days) * 1 days; + uint256 daysElapsed = today - launchDay; + + // Penalty scales linearly: for each day, add PENALTY_PER_DAY_BP bp (1e18 = 100%) + uint256 penalty = (daysElapsed * PENALTY_PER_DAY_BP) / 1e18; + uint256 et = VESTING_DURATION + launchDay + penalty; + return et; + } +} diff --git a/contracts/vesting/SymmVestingV2.sol b/contracts/vesting/SymmVestingV2.sol new file mode 100644 index 0000000..36bf969 --- /dev/null +++ b/contracts/vesting/SymmVestingV2.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +import "./interfaces/IPermit2.sol"; +import "./interfaces/IMintableERC20.sol"; +import { IPool } from "./interfaces/IPool.sol"; +import { IRouter } from "./interfaces/IRouter.sol"; +import "./VestingV2.sol"; + +/// @title SymmVesting Contract +/// @notice Extends Vesting to add liquidity functionality for SYMM and SYMM LP tokens. +/// @dev Inherits pausable functionality and vesting plan management from Vesting. +contract SymmVestingV2 is VestingV2 { + using SafeERC20 for IERC20; + using VestingPlanOps for VestingPlan; + + //-------------------------------------------------------------------------- + // Events + //-------------------------------------------------------------------------- + + /// @notice Emitted when liquidity is added. + /// @param user The address adding liquidity. + /// @param symmAmount The amount of SYMM used. + /// @param usdcAmount The amount of USDC required. + /// @param lpAmount The amount of LP tokens received. + event LiquidityAdded(address indexed user, uint256 symmAmount, uint256 usdcAmount, uint256 lpAmount); + + //-------------------------------------------------------------------------- + // Errors + //-------------------------------------------------------------------------- + + error SlippageExceeded(); + error ZeroDivision(); + error MaxUsdcExceeded(); + error VestingNotStarted(); + + //-------------------------------------------------------------------------- + // State Variables + //-------------------------------------------------------------------------- + + IPool public POOL; + IRouter public ROUTER; + IPermit2 public PERMIT2; + address public VAULT; + address public SYMM; + address public USDC; + address public SYMM_LP; + + //-------------------------------------------------------------------------- + // Initialization + //-------------------------------------------------------------------------- + + /// @notice Initializes the SymmVesting contract. + /// @param admin Address to receive the admin and role assignments. + /// @param _lockedClaimPenaltyReceiver Address that receives the locked claim penalty. + function initialize( + address admin, + address _lockedClaimPenaltyReceiver, + address _pool, + address _router, + address _permit2, + address _vault, + address _symm, + address _usdc, + address _symm_lp + ) public initializer { + if ( + admin == address(0) || + _lockedClaimPenaltyReceiver == address(0) || + _pool == address(0) || + _router == address(0) || + _permit2 == address(0) || + _vault == address(0) || + _symm == address(0) || + _usdc == address(0) || + _symm_lp == address(0) + ) revert ZeroAddress(); + __vesting_init(admin, 500000000000000000, _lockedClaimPenaltyReceiver); + POOL = IPool(_pool); + ROUTER = IRouter(_router); + PERMIT2 = IPermit2(_permit2); + VAULT = _vault; + SYMM = _symm; + USDC = _usdc; + SYMM_LP = _symm_lp; + } + + //-------------------------------------------------------------------------- + // Liquidity for Vesting Functions + //-------------------------------------------------------------------------- + + /// @notice Adds liquidity by converting a portion of SYMM vesting into SYMM LP tokens. + /// @dev Claims any unlocked tokens from SYMM and SYMM LP vesting plans. + /// Reverts if the SYMM vesting plan's locked amount is insufficient. + /// @param amount The amount of SYMM to use for adding liquidity. + /// @param minLpAmount The minimum acceptable LP token amount to receive (for slippage protection). + /// @param maxUsdcIn The maximum amount of USDC that can be used (for price protection). + /// @return amountsIn Array of token amounts used (SYMM and USDC). + /// @return lpAmount The amount of LP tokens minted. + function addLiquidity( + uint256 amount, + uint256 minLpAmount, + uint256 maxUsdcIn, + uint256 planId + ) external whenNotPaused nonReentrant returns (uint256[] memory amountsIn, uint256 lpAmount) { + return _addLiquidityProcess(amount, minLpAmount, maxUsdcIn, planId); + } + + /// @notice Adds liquidity by converting a portion of SYMM vesting into SYMM LP tokens. + /// @dev Claims any unlocked tokens from SYMM and SYMM LP vesting plans. + /// Reverts if the SYMM vesting plan's locked amount is insufficient. + /// @param percentage The percentage of locked SYMM to use for adding liquidity. + /// @param minLpAmount The minimum acceptable LP token amount to receive (for slippage protection). + /// @param maxUsdcIn The maximum amount of USDC that can be used (for price protection). + /// @return amountsIn Array of token amounts used (SYMM and USDC). + /// @return lpAmount The amount of LP tokens minted. + function addLiquidityByPercentage( + uint256 percentage, + uint256 minLpAmount, + uint256 maxUsdcIn, + uint256 planId + ) external whenNotPaused nonReentrant returns (uint256[] memory amountsIn, uint256 lpAmount) { + uint256 amount = (getTotalLockedAmount(msg.sender, SYMM) * percentage) / 1e18; + return _addLiquidityProcess(amount, minLpAmount, maxUsdcIn, planId); + } + + function _addLiquidityProcess( + uint256 amount, + uint256 minLpAmount, + uint256 maxUsdcIn, + uint256 planId + ) internal returns (uint256[] memory amountsIn, uint256 lpAmount) { + // Claim any unlocked SYMM tokens first. + _claimUnlockedToken(SYMM, msg.sender, planId); + + VestingPlan storage symmVestingPlan = vestingPlans[SYMM][msg.sender][planId]; + + if (symmVestingPlan.startTime > block.timestamp) revert VestingNotStarted(); + + uint256 symmLockedAmount = symmVestingPlan.lockedAmount(); + if (symmLockedAmount < amount) revert InvalidAmount(); + + _ensureSufficientBalance(SYMM, amount); + + // Add liquidity to the pool. + (amountsIn, lpAmount) = _addLiquidity(amount, minLpAmount, maxUsdcIn); + + // Update SYMM vesting plan by reducing the locked amount. + symmVestingPlan.resetAmount(symmLockedAmount - amountsIn[0]); + + // Claim any unlocked SYMM LP tokens. + _claimUnlockedToken(SYMM_LP, msg.sender, planId); + + VestingPlan storage lpVestingPlan = vestingPlans[SYMM_LP][msg.sender][planId]; + + address[] memory users = new address[](1); + users[0] = msg.sender; + uint256[] memory amounts = new uint256[](1); + amounts[0] = lpVestingPlan.lockedAmount() + lpAmount; + uint256[] memory planIds = new uint256[](1); + planIds[0] = planId; + + // Increase the locked amount by the received LP tokens. + if (lpVestingPlan.isSetup()) { + _resetVestingPlans(SYMM_LP, users, amounts, planIds); + } else { + _setupVestingPlans(SYMM_LP, block.timestamp, symmVestingPlan.endTime, users, amounts); + } + + emit LiquidityAdded(msg.sender, amountsIn[0], amountsIn[1], lpAmount); + } + + /// @notice Internal function to add liquidity using a specified amount of SYMM. + /// @dev Transfers USDC from the caller, approves token spending for the VAULT, and interacts with the liquidity router. + /// @param symmIn The amount of SYMM to contribute. + /// @param minLpAmount The minimum acceptable LP token amount to receive (for slippage protection). + /// @param maxUsdcIn The maximum amount of USDC that can be used (for price protection). + /// @return amountsIn Array containing the amounts of SYMM and USDC used. + /// @return lpAmount The number of LP tokens minted. + function _addLiquidity(uint256 symmIn, uint256 minLpAmount, uint256 maxUsdcIn) internal returns (uint256[] memory amountsIn, uint256 lpAmount) { + (uint256 usdcIn, uint256 expectedLpAmount) = getLiquidityQuote(symmIn); + + // Check if usdcIn exceeds maxUsdcIn parameter + if (maxUsdcIn > 0 && usdcIn > maxUsdcIn) revert MaxUsdcExceeded(); + + uint256 minLpAmountWithSlippage = minLpAmount > 0 ? minLpAmount : (expectedLpAmount * 99) / 100; // Default 1% slippage if not specified + + // Retrieve pool tokens. Assumes poolTokens[0] is SYMM and poolTokens[1] is USDC. + IERC20[] memory poolTokens = POOL.getTokens(); + (IERC20 symm, IERC20 usdc) = (poolTokens[0], poolTokens[1]); + + // Pull USDC from the user and approve the VAULT. + usdc.safeTransferFrom(msg.sender, address(this), usdcIn); + usdc.approve(address(PERMIT2), usdcIn); + symm.approve(address(PERMIT2), symmIn); + PERMIT2.approve(SYMM, address(ROUTER), uint160(symmIn), uint48(block.timestamp)); + PERMIT2.approve(USDC, address(ROUTER), uint160(usdcIn), uint48(block.timestamp)); + + amountsIn = new uint256[](2); + amountsIn[0] = symmIn; + amountsIn[1] = usdcIn; + + uint256 initialLpBalance = IERC20(SYMM_LP).balanceOf(address(this)); + + // Call the router to add liquidity. + amountsIn = ROUTER.addLiquidityProportional( + address(POOL), + amountsIn, + expectedLpAmount, + false, // wethIsEth: bool + "" // userData: bytes + ); + + // Return unused usdc + if (usdcIn - amountsIn[1] > 0) usdc.safeTransfer(msg.sender, usdcIn - amountsIn[1]); + + // Calculate actual LP tokens received by comparing balances. + uint256 newLpBalance = IERC20(SYMM_LP).balanceOf(address(this)); + lpAmount = newLpBalance - initialLpBalance; + + if (lpAmount < minLpAmountWithSlippage) revert SlippageExceeded(); + } + + /// @notice Calculates the ceiling of (a * b) divided by c. + /// @dev Computes ceil(a * b / c) using the formula (a * b - 1) / c + 1 when the product is nonzero. + /// Returns 0 if a * b equals 0. + /// @param a The multiplicand. + /// @param b The multiplier. + /// @param c The divisor. + /// @return result The smallest integer greater than or equal to (a * b) / c. + function _mulDivUp(uint256 a, uint256 b, uint256 c) internal pure returns (uint256 result) { + // This check is required because Yul's div doesn't revert on c==0. + if (c == 0) revert ZeroDivision(); + + // Multiple overflow protection is done by Solidity 0.8.x. + uint256 product = a * b; + + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, if x == 0 then the result is zero + // + // Equivalent to: + // result = a == 0 ? 0 : (a * b - 1) / c + 1 + assembly ("memory-safe") { + result := mul(iszero(iszero(product)), add(div(sub(product, 1), c), 1)) + } + } + + /// @notice Calculates the USDC required and LP tokens expected for a given SYMM amount. + /// @dev Uses current pool balances and total supply to compute the liquidity parameters. + /// @param symmAmount The amount of SYMM. + /// @return usdcAmount The USDC required. + /// @return lpAmount The LP tokens that will be minted. + function getLiquidityQuote(uint256 symmAmount) public view returns (uint256 usdcAmount, uint256 lpAmount) { + uint256[] memory balances = POOL.getCurrentLiveBalances(); + uint256 totalSupply = POOL.totalSupply(); + uint256 symmBalance = balances[0]; + uint256 usdcBalance = balances[1]; + + usdcAmount = (symmAmount * usdcBalance) / symmBalance; + usdcAmount = _mulDivUp(usdcAmount, 1e18, 1e30); + lpAmount = (symmAmount * totalSupply) / symmBalance; + } + + function _mintTokenIfPossible(address token, uint256 amount) internal override { + if (token == SYMM) IMintableERC20(token).mint(address(this), amount); + } +} diff --git a/contracts/vesting/VestingUpsertManager.sol b/contracts/vesting/VestingUpsertManager.sol new file mode 100644 index 0000000..0946e4d --- /dev/null +++ b/contracts/vesting/VestingUpsertManager.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +import "./interfaces/IVesting.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; + +interface ISymmVestingPlanInitializer { + function endTimeStartsAt(uint256 _timestamp) external view returns (uint256); +} + +contract VestingUpsertManager is Initializable, AccessControlEnumerableUpgradeable { + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + IVesting public vesting; + ISymmVestingPlanInitializer public planInitializer; + + function initialize(address admin, address operator, address vestingAddress, address vestingPlanAddress) public initializer { + __AccessControlEnumerable_init(); + vesting = IVesting(vestingAddress); + planInitializer = ISymmVestingPlanInitializer(vestingPlanAddress); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(OPERATOR_ROLE, operator); + } + + function upsertVestingPlans(address token, address[] calldata users, uint256[] calldata newAmounts) external onlyRole(OPERATOR_ROLE) { + require(users.length == newAmounts.length, "VestingUpsertManager: Mismatched lengths"); + require(token != address(0), "VestingUpsertManager: Zero address"); + + uint256 startTime = block.timestamp; + uint256 endTime = planInitializer.endTimeStartsAt(startTime); + + address[] memory toSetup = new address[](users.length); + uint256[] memory toSetupAmounts = new uint256[](users.length); + uint256 setupCount = 0; + + address[] memory toReset = new address[](users.length); + uint256[] memory toResetAmounts = new uint256[](users.length); + uint256 resetCount = 0; + + for (uint256 i = 0; i < users.length; i++) { + (uint256 existingAmount, , , ) = vesting.vestingPlans(token, users[i]); + + if (existingAmount == 0) { + toSetup[setupCount] = users[i]; + toSetupAmounts[setupCount] = newAmounts[i]; + setupCount++; + } else { + uint256 locked = vesting.getLockedAmountsForToken(users[i], token); + toReset[resetCount] = users[i]; + toResetAmounts[resetCount] = locked + newAmounts[i]; + resetCount++; + } + } + + if (setupCount > 0) vesting.setupVestingPlans(token, startTime, endTime, _slice(toSetup, setupCount), _slice(toSetupAmounts, setupCount)); + if (resetCount > 0) vesting.resetVestingPlans(token, _slice(toReset, resetCount), _slice(toResetAmounts, resetCount)); + } + + function _slice(address[] memory input, uint256 length) internal pure returns (address[] memory) { + address[] memory output = new address[](length); + for (uint256 i = 0; i < length; i++) output[i] = input[i]; + return output; + } + + function _slice(uint256[] memory input, uint256 length) internal pure returns (uint256[] memory) { + uint256[] memory output = new uint256[](length); + for (uint256 i = 0; i < length; i++) output[i] = input[i]; + return output; + } +} diff --git a/contracts/vesting/VestingV2.sol b/contracts/vesting/VestingV2.sol new file mode 100644 index 0000000..6dcaa61 --- /dev/null +++ b/contracts/vesting/VestingV2.sol @@ -0,0 +1,530 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +/** + * @title VestingV2 + * @notice Advanced token vesting contract with linear vesting schedules, penalty mechanisms, + * and comprehensive claim options. Supports multiple vesting plans per user per token + * with flexible claiming of both unlocked and locked tokens. + * + * @dev Core features include: + * • Multiple vesting plans per user per token with unique plan IDs + * • Linear vesting schedules with configurable start and end times + * • Unlocked token claiming for fully vested amounts + * • Locked token claiming with configurable penalty rates for early withdrawal + * • Percentage-based claiming for flexible withdrawal amounts + * • Plan reset functionality with automatic unlocked token claiming + * • Role-based access control for plan management and operations + * • Virtual minting hooks for token supply management + * • Comprehensive view functions for vesting analytics and monitoring + * • Upgradeable architecture with proper storage gap management + * + * The contract integrates with LibVestingPlan for vesting calculations and uses + * OpenZeppelin's upgradeable contracts for security and access control. + */ + +import {VestingPlanOps, VestingPlan} from "./libraries/LibVestingPlan.sol"; +import {AccessControlEnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +contract VestingV2 is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20; + using VestingPlanOps for VestingPlan; + + /* ─────────────────────────────── Roles ─────────────────────────────── */ + + /// @notice Role for setting up and resetting vesting plans. + bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); + + /// @notice Role for claiming tokens on behalf of users. + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + /// @notice Role for pausing contract operations. + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// @notice Role for unpausing contract operations. + bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); + + /* ──────────────────────── Storage Variables ──────────────────────── */ + + /// @notice Triple mapping storing vesting plan details: token => user => planId => VestingPlan. + mapping(address => mapping(address => mapping(uint256 => VestingPlan))) public vestingPlans; + + /// @notice Double mapping tracking number of vesting plans: token => user => count. + mapping(address => mapping(address => uint256)) public userVestingPlanCount; + + /// @notice Mapping tracking total vested amount per token across all plans. + mapping(address => uint256) public totalVested; + + /// @notice Penalty rate for claiming locked tokens early (scaled by 1e18). + /// @dev Example: 0.1e18 represents a 10% penalty on early claims. + uint256 public lockedClaimPenalty; + + /// @notice Address that receives penalties from early claims of locked tokens. + address public lockedClaimPenaltyReceiver; + + /// @dev Reserved storage slots for future upgrades to prevent storage collisions. + uint256[50] private __gap; + + /* ─────────────────────────────── Events ─────────────────────────────── */ + + /** + * @notice Emitted when a new vesting plan is created for a user. + * @param token Address of the token being vested. + * @param user Address of the user receiving the vesting plan. + * @param planId ID of the vesting plan. + * @param amount Total amount of tokens in the vesting plan. + * @param startTime Start time of the vesting period. + * @param endTime End time of the vesting period. + */ + event VestingPlanSetup(address indexed token, address indexed user, uint256 indexed planId, uint256 amount, uint256 startTime, uint256 endTime); + + /** + * @notice Emitted when a vesting plan is reset with a new amount. + * @param token Address of the token being vested. + * @param user Address of the user whose plan is reset. + * @param planId ID of the vesting plan. + * @param newAmount New total amount of tokens in the vesting plan. + */ + event VestingPlanReset(address indexed token, address indexed user, uint256 indexed planId, uint256 newAmount); + + /** + * @notice Emitted when unlocked tokens are claimed from a vesting plan. + * @param token Address of the token being claimed. + * @param user Address of the user claiming the tokens. + * @param planId ID of the vesting plan. + * @param amount Amount of tokens claimed. + */ + event UnlockedTokenClaimed(address indexed token, address indexed user, uint256 indexed planId, uint256 amount); + + /** + * @notice Emitted when locked tokens are claimed with a penalty. + * @param token Address of the token being claimed. + * @param user Address of the user claiming the tokens. + * @param planId ID of the vesting plan. + * @param amount Total amount of tokens claimed (before penalty). + * @param penalty Penalty amount deducted from the claim. + */ + event LockedTokenClaimed(address indexed token, address indexed user, uint256 indexed planId, uint256 amount, uint256 penalty); + + /* ─────────────────────────────── Errors ─────────────────────────────── */ + + error MismatchArrays(); // input arrays have mismatched lengths + error InvalidAmount(); // invalid amount provided (e.g., exceeds locked amount) + error ZeroAddress(); // zero address provided for critical parameters + error InvalidPlanId(); // invalid vesting plan ID provided + + /* ─────────────────────────── Initialization ─────────────────────────── */ + + /** + * @notice Initialize the vesting contract with initial configuration. + * @param admin Address to receive admin and all role assignments. + * @param _lockedClaimPenalty Penalty rate for early claims (scaled by 1e18). + * @param _lockedClaimPenaltyReceiver Address to receive penalties from early claims. + * + * @dev Sets up access control, pausing, and reentrancy guard. Validates addresses + * to prevent zero-address configuration. + */ + function __vesting_init(address admin, uint256 _lockedClaimPenalty, address _lockedClaimPenaltyReceiver) public onlyInitializing { + __AccessControlEnumerable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + lockedClaimPenalty = _lockedClaimPenalty; + lockedClaimPenaltyReceiver = _lockedClaimPenaltyReceiver; + + // Validate addresses to prevent zero-address configuration + if (admin == address(0) || _lockedClaimPenaltyReceiver == address(0)) revert ZeroAddress(); + + // Grant all roles to the admin + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(SETTER_ROLE, admin); + _grantRole(PAUSER_ROLE, admin); + _grantRole(UNPAUSER_ROLE, admin); + _grantRole(OPERATOR_ROLE, admin); + } + + /* ────────────────────── Pausing Functions ────────────────────── */ + + /** + * @notice Pause the contract, disabling state-changing functions. + * @dev Only callable by accounts with PAUSER_ROLE. + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @notice Unpause the contract, enabling state-changing functions. + * @dev Only callable by accounts with UNPAUSER_ROLE. + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } + + /* ────────────── Vesting Plan Management ────────────── */ + + /** + * @notice Set up vesting plans for multiple users with specified parameters. + * @param token Address of the token to vest. + * @param startTime Start time of the vesting period. + * @param endTime End time of the vesting period. + * @param users Array of user addresses to receive vesting plans. + * @param amounts Array of token amounts for each vesting plan. + * + * @dev Creates new vesting plans with sequential plan IDs. Tokens must be available + * in the contract or minting hook must handle deficits. + */ + function setupVestingPlans( + address token, + uint256 startTime, + uint256 endTime, + address[] memory users, + uint256[] memory amounts + ) external onlyRole(SETTER_ROLE) whenNotPaused nonReentrant returns (uint256[] memory) { + return _setupVestingPlans(token, startTime, endTime, users, amounts); + } + + /** + * @notice Reset existing vesting plans for multiple users with new amounts. + * @param token Address of the token being vested. + * @param users Array of user addresses whose plans are being reset. + * @param planIds Array of vesting plan IDs to reset. + * @param amounts Array of new token amounts for each plan. + * + * @dev Claims any unlocked tokens before resetting. Validates plan ID existence. + */ + function resetVestingPlans( + address token, + address[] memory users, + uint256[] memory planIds, + uint256[] memory amounts + ) external onlyRole(SETTER_ROLE) whenNotPaused nonReentrant { + _resetVestingPlans(token, users, planIds, amounts); + } + + /* ───────────────── Token Claim Functions ───────────────── */ + + /** + * @notice Claim unlocked tokens for the caller from a specific vesting plan. + * @param token Address of the token to claim. + * @param planId ID of the vesting plan. + * + * @dev Claims only fully vested tokens without penalty. + */ + function claimUnlockedToken(address token, uint256 planId) external whenNotPaused nonReentrant { + _claimUnlockedToken(token, msg.sender, planId); + } + + /** + * @notice Claim unlocked tokens on behalf of a user from a specific vesting plan. + * @param token Address of the token to claim. + * @param user Address of the user to claim for. + * @param planId ID of the vesting plan. + * + * @dev Only callable by accounts with OPERATOR_ROLE. + */ + function claimUnlockedTokenFor(address token, address user, uint256 planId) external onlyRole(OPERATOR_ROLE) whenNotPaused nonReentrant { + _claimUnlockedToken(token, user, planId); + } + + /** + * @notice Claim locked tokens for the caller with penalty deduction. + * @param token Address of the token to claim. + * @param planId ID of the vesting plan. + * @param amount Amount of locked tokens to claim. + * + * @dev Claims unlocked tokens first, then processes locked amount with penalty. + */ + function claimLockedToken(address token, uint256 planId, uint256 amount) external whenNotPaused nonReentrant { + _claimLockedToken(token, msg.sender, planId, amount); + } + + /** + * @notice Claim a percentage of locked tokens for the caller. + * @param token Address of the token to claim. + * @param planId ID of the vesting plan. + * @param percentage Percentage of locked tokens to claim (scaled by 1e18). + * + * @dev Calculates claim amount based on percentage of current locked balance. + */ + function claimLockedTokenByPercentage(address token, uint256 planId, uint256 percentage) external whenNotPaused nonReentrant { + uint256 lockedAmount = getLockedAmountForPlan(msg.sender, token, planId); + uint256 amountToClaim = (lockedAmount * percentage) / 1e18; + _claimLockedToken(token, msg.sender, planId, amountToClaim); + } + + /** + * @notice Claim locked tokens on behalf of a user with penalty deduction. + * @param token Address of the token to claim. + * @param user Address of the user to claim for. + * @param planId ID of the vesting plan. + * @param amount Amount of locked tokens to claim. + * + * @dev Only callable by accounts with OPERATOR_ROLE. + */ + function claimLockedTokenFor( + address token, + address user, + uint256 planId, + uint256 amount + ) external onlyRole(OPERATOR_ROLE) whenNotPaused nonReentrant { + _claimLockedToken(token, user, planId, amount); + } + + /** + * @notice Claim a percentage of locked tokens on behalf of a user. + * @param token Address of the token to claim. + * @param user Address of the user to claim for. + * @param planId ID of the vesting plan. + * @param percentage Percentage of locked tokens to claim (scaled by 1e18). + * + * @dev Only callable by accounts with OPERATOR_ROLE. + */ + function claimLockedTokenForByPercentage( + address token, + address user, + uint256 planId, + uint256 percentage + ) external onlyRole(OPERATOR_ROLE) whenNotPaused nonReentrant { + uint256 lockedAmount = getLockedAmountForPlan(user, token, planId); + uint256 amountToClaim = (lockedAmount * percentage) / 1e18; + _claimLockedToken(token, user, planId, amountToClaim); + } + + /* ───────────────────────── Internal Helpers ───────────────────────── */ + + /** + * @dev Internal function to set up vesting plans for multiple users. + * @param token Address of the token to vest. + * @param startTime Start time of the vesting period. + * @param endTime End time of the vesting period. + * @param users Array of user addresses. + * @param amounts Array of token amounts for each vesting plan. + * + * @dev Creates sequential plan IDs and updates total vested amounts. + */ + function _setupVestingPlans( + address token, + uint256 startTime, + uint256 endTime, + address[] memory users, + uint256[] memory amounts + ) internal returns (uint256[] memory) { + if (users.length != amounts.length) revert MismatchArrays(); + uint256 len = users.length; + uint256[] memory planIds = new uint256[](len); + // Iterate through users to set up individual vesting plans + for (uint256 i = 0; i < len; i++) { + address user = users[i]; + uint256 amount = amounts[i]; + uint256 planId = userVestingPlanCount[token][user]; + + // Update total vested amount and create new vesting plan + totalVested[token] += amount; + VestingPlan storage vestingPlan = vestingPlans[token][user][planId]; + vestingPlan.setup(amount, startTime, endTime); + + planIds[i] = planId; + // Increment plan count for the user + userVestingPlanCount[token][user]++; + emit VestingPlanSetup(token, user, planId, amount, startTime, endTime); + } + return planIds; + } + + /** + * @dev Internal function to reset vesting plans for multiple users. + * @param token Address of the token being vested. + * @param users Array of user addresses. + * @param planIds Array of vesting plan IDs to reset. + * @param amounts Array of new token amounts. + * + * @dev Claims unlocked tokens before resetting and updates total vested amounts. + */ + function _resetVestingPlans(address token, address[] memory users, uint256[] memory planIds, uint256[] memory amounts) internal { + if (users.length != amounts.length || users.length != planIds.length) revert MismatchArrays(); + uint256 len = users.length; + // Iterate through users to reset vesting plans + for (uint256 i = 0; i < len; i++) { + address user = users[i]; + uint256 planId = planIds[i]; + uint256 amount = amounts[i]; + + if (planId >= userVestingPlanCount[token][user]) revert InvalidPlanId(); + + // Claim any unlocked tokens before resetting + _claimUnlockedToken(token, user, planId); + + VestingPlan storage vestingPlan = vestingPlans[token][user][planId]; + uint256 oldTotal = vestingPlan.lockedAmount() + vestingPlan.unlockedAmount(); // Total before reset + vestingPlan.resetAmount(amount); + // Update total vested amount + totalVested[token] = totalVested[token] - oldTotal + amount; + emit VestingPlanReset(token, user, planId, amount); + } + } + + /** + * @dev Ensure the contract has sufficient token balance, minting if necessary. + * @param token Address of the token. + * @param amount Required amount of tokens. + * + * @dev Calls virtual minting hook if balance is insufficient. + */ + function _ensureSufficientBalance(address token, uint256 amount) internal virtual { + uint256 currentBalance = IERC20(token).balanceOf(address(this)); + if (currentBalance < amount) { + uint256 deficit = amount - currentBalance; + // Attempt to mint tokens to cover the deficit + _mintTokenIfPossible(token, deficit); + } + } + + /** + * @dev Virtual hook to mint tokens if supported by the token contract. + * @param token Address of the token to mint. + * @param amount Amount of tokens to mint. + * + * @dev Default implementation is a no-op. Override in derived contracts to enable minting. + */ + function _mintTokenIfPossible(address token, uint256 amount) internal virtual { + // No-op in base implementation + } + + /** + * @dev Internal function to claim unlocked tokens from a vesting plan. + * @param token Address of the token to claim. + * @param user Address of the user claiming tokens. + * @param planId ID of the vesting plan. + * + * @dev Transfers claimable tokens and updates plan state and total vested amounts. + */ + function _claimUnlockedToken(address token, address user, uint256 planId) internal { + if (planId >= userVestingPlanCount[token][user]) revert InvalidPlanId(); + VestingPlan storage vestingPlan = vestingPlans[token][user][planId]; + uint256 claimableAmount = vestingPlan.claimable(); + if (claimableAmount == 0) return; + + // Update vesting plan and total vested amount + totalVested[token] -= claimableAmount; + vestingPlan.claimedAmount += claimableAmount; + + // Ensure sufficient balance before transfer + _ensureSufficientBalance(token, claimableAmount); + IERC20(token).safeTransfer(user, claimableAmount); + emit UnlockedTokenClaimed(token, user, planId, claimableAmount); + } + + /** + * @dev Internal function to claim locked tokens with penalty deduction. + * @param token Address of the token to claim. + * @param user Address of the user claiming tokens. + * @param planId ID of the vesting plan. + * @param amount Amount of locked tokens to claim. + * + * @dev Claims unlocked tokens first, then processes locked amount with penalty. + */ + function _claimLockedToken(address token, address user, uint256 planId, uint256 amount) internal { + // Claim any unlocked tokens first + _claimUnlockedToken(token, user, planId); + + if (planId >= userVestingPlanCount[token][user]) revert InvalidPlanId(); + VestingPlan storage vestingPlan = vestingPlans[token][user][planId]; + + if (vestingPlan.lockedAmount() < amount) revert InvalidAmount(); + + // Update vesting plan and total vested amount + uint256 newTotalAmount = vestingPlan.amount - amount; + vestingPlan.resetAmount(newTotalAmount); + totalVested[token] -= amount; + + // Calculate and apply penalty + uint256 penalty = (amount * lockedClaimPenalty) / 1e18; + _ensureSufficientBalance(token, amount); + IERC20(token).safeTransfer(user, amount - penalty); + IERC20(token).safeTransfer(lockedClaimPenaltyReceiver, penalty); + emit LockedTokenClaimed(token, user, planId, amount, penalty); + } + + /* ────────────────────────── View Functions ────────────────────────── */ + + /** + * @notice Get the locked token amount for a specific vesting plan. + * @param user Address of the user. + * @param token Address of the token. + * @param planId ID of the vesting plan. + * @return Amount of tokens still locked in the plan. + */ + function getLockedAmountForPlan(address user, address token, uint256 planId) public view returns (uint256) { + if (planId >= userVestingPlanCount[token][user]) return 0; + return vestingPlans[token][user][planId].lockedAmount(); + } + + /** + * @notice Get the claimable token amount for a specific vesting plan. + * @param user Address of the user. + * @param token Address of the token. + * @param planId ID of the vesting plan. + * @return Amount of tokens currently claimable from the plan. + */ + function getClaimableAmountForPlan(address user, address token, uint256 planId) public view returns (uint256) { + if (planId >= userVestingPlanCount[token][user]) return 0; + return vestingPlans[token][user][planId].claimable(); + } + + /** + * @notice Get the unlocked token amount for a specific vesting plan. + * @param user Address of the user. + * @param token Address of the token. + * @param planId ID of the vesting plan. + * @return Total amount of tokens that have been unlocked in the plan. + */ + function getUnlockedAmountForPlan(address user, address token, uint256 planId) public view returns (uint256) { + if (planId >= userVestingPlanCount[token][user]) return 0; + return vestingPlans[token][user][planId].unlockedAmount(); + } + + /** + * @notice Get the total locked tokens for a user across all vesting plans for a token. + * @param user Address of the user. + * @param token Address of the token. + * @return totalLocked Total amount of locked tokens across all plans. + */ + function getTotalLockedAmount(address user, address token) public view returns (uint256 totalLocked) { + uint256 count = userVestingPlanCount[token][user]; + for (uint256 i = 0; i < count; i++) { + totalLocked += getLockedAmountForPlan(user, token, i); + } + } + + /** + * @notice Get the total claimable tokens for a user across all vesting plans for a token. + * @param user Address of the user. + * @param token Address of the token. + * @return totalClaimable Total amount of claimable tokens across all plans. + */ + function getTotalClaimableAmount(address user, address token) public view returns (uint256 totalClaimable) { + uint256 count = userVestingPlanCount[token][user]; + for (uint256 i = 0; i < count; i++) { + totalClaimable += getClaimableAmountForPlan(user, token, i); + } + } + + /** + * @notice Get the total unlocked tokens for a user across all vesting plans for a token. + * @param user Address of the user. + * @param token Address of the token. + * @return totalUnlocked Total amount of unlocked tokens across all plans. + */ + function getTotalUnlockedAmount(address user, address token) public view returns (uint256 totalUnlocked) { + uint256 count = userVestingPlanCount[token][user]; + for (uint256 i = 0; i < count; i++) { + totalUnlocked += getUnlockedAmountForPlan(user, token, i); + } + } +} diff --git a/contracts/vesting/interfaces/IVesting.sol b/contracts/vesting/interfaces/IVesting.sol new file mode 100644 index 0000000..322a89b --- /dev/null +++ b/contracts/vesting/interfaces/IVesting.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +interface IVesting { + function pause() external; + function unpause() external; + + function resetVestingPlans(address token, address[] memory users, uint256[] memory amounts) external; + function setupVestingPlans(address token, uint256 startTime, uint256 endTime, address[] memory users, uint256[] memory amounts) external; + + function claimUnlockedToken(address token) external; + function claimUnlockedTokenFor(address token, address user) external; + + function claimLockedToken(address token, uint256 amount) external; + function claimLockedTokenByPercentage(address token, uint256 percentage) external; + + function claimLockedTokenFor(address token, address user, uint256 amount) external; + function claimLockedTokenForByPercentage(address token, address user, uint256 percentage) external; + + function getLockedAmountsForToken(address user, address token) external view returns (uint256); + function getClaimableAmountsForToken(address user, address token) external view returns (uint256); + function getUnlockedAmountForToken(address user, address token) external view returns (uint256); + + function lockedClaimPenalty() external view returns (uint256); + function lockedClaimPenaltyReceiver() external view returns (address); + function vestingPlans(address token, address user) external view returns ( + uint256 totalAmount, + uint256 claimedAmount, + uint256 startTime, + uint256 endTime + ); + function totalVested(address token) external view returns (uint256); +} diff --git a/contracts/vesting/libraries/LibVestingPlan.sol b/contracts/vesting/libraries/LibVestingPlan.sol index 2da6aab..3ccc519 100644 --- a/contracts/vesting/libraries/LibVestingPlan.sol +++ b/contracts/vesting/libraries/LibVestingPlan.sol @@ -17,7 +17,7 @@ library VestingPlanOps { /// @notice Calculates the unlocked amount for a vesting plan. /// @param self The vesting plan. /// @return The unlocked token amount. - function unlockedAmount(VestingPlan storage self) public view returns (uint256) { + function unlockedAmount(VestingPlan storage self) internal view returns (uint256) { uint256 currentTime = block.timestamp; if (currentTime >= self.endTime) return self.amount; if (currentTime <= self.startTime) return 0; @@ -29,21 +29,21 @@ library VestingPlanOps { /// @notice Calculates the locked token amount. /// @param self The vesting plan. /// @return The locked token amount. - function lockedAmount(VestingPlan storage self) public view returns (uint256) { + function lockedAmount(VestingPlan storage self) internal view returns (uint256) { return self.amount - unlockedAmount(self); } /// @notice Calculates the claimable amount. /// @param self The vesting plan. /// @return The claimable token amount. - function claimable(VestingPlan storage self) public view returns (uint256) { + function claimable(VestingPlan storage self) internal view returns (uint256) { return unlockedAmount(self) - self.claimedAmount; } /// @notice Returns the remaining duration of the vesting plan. /// @param self The vesting plan. /// @return The number of seconds remaining. - function remainingDuration(VestingPlan storage self) public view returns (uint256) { + function remainingDuration(VestingPlan storage self) internal view returns (uint256) { if (block.timestamp <= self.startTime) return self.endTime - self.startTime; return self.endTime > block.timestamp ? self.endTime - block.timestamp : 0; } @@ -55,7 +55,7 @@ library VestingPlanOps { /// @param startTime Start time of vesting. /// @param endTime End time of vesting. /// @return The updated vesting plan. - function setup(VestingPlan storage self, uint256 amount, uint256 startTime, uint256 endTime) public returns (VestingPlan storage) { + function setup(VestingPlan storage self, uint256 amount, uint256 startTime, uint256 endTime) internal returns (VestingPlan storage) { if (isSetup(self)) revert AlreadySetup(); self.startTime = startTime; self.endTime = endTime; @@ -69,7 +69,7 @@ library VestingPlanOps { /// @param self The vesting plan. /// @param amount The new total token amount. /// @return The updated vesting plan. - function resetAmount(VestingPlan storage self, uint256 amount) public returns (VestingPlan storage) { + function resetAmount(VestingPlan storage self, uint256 amount) internal returns (VestingPlan storage) { if (claimable(self) != 0) revert ShouldClaimFirst(); if (!isSetup(self)) revert ShouldSetupFirst(); // Rebase the vesting plan from now. @@ -86,7 +86,7 @@ library VestingPlanOps { /// @notice Checks if a vesting plan is already set up. /// @param self The vesting plan. /// @return True if the vesting plan is set up, false otherwise. - function isSetup(VestingPlan storage self) public view returns (bool) { + function isSetup(VestingPlan storage self) internal view returns (bool) { return self.amount != 0; } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 833fada..f513234 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -7,14 +7,10 @@ import "hardhat-gas-reporter" import { HardhatUserConfig } from "hardhat/config" import "solidity-coverage" -import "./tasks/symmAllocationClaimer" -import "./tasks/symmioToken" -import "./tasks/symmVesting" -import "./tasks/symmStaking"; - +import "./tasks" dotenv.config() -const accounts_list: any = [process.env.ACCOUNT] +const accounts_list: any = [process.env.ACCOUNT || "0xec81e00837948239d5927bcb2b785675552bc92f1d2607ee91c540ddb56d6796"] // Dummy private key export const config: HardhatUserConfig = { defaultNetwork: "hardhat", @@ -47,17 +43,17 @@ export const config: HardhatUserConfig = { networks: { hardhat: { - forking: { - url: "", - blockNumber: 26800831, - }, + // forking: { + // url: "https://1rpc.io/base", + // blockNumber: 33113717, + // }, }, ethereum: { url: "https://ethereum.blockpi.network/v1/rpc/public", accounts: accounts_list, }, base: { - url: "https://mainnet.base.org", + url: "https://base.llamarpc.com", accounts: accounts_list, }, polygon: { diff --git a/package-lock.json b/package-lock.json index d909bc6..a542278 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,8 @@ "name": "symm", "version": "1.0.0", "dependencies": { - "csv-parse": "^5.6.0" - }, - "dependencies": { - "csv-parse": "^5.6.0" + "csv-parse": "^5.6.0", + "prettier-plugin-solidity": "^2.1.0" }, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.7", @@ -3463,7 +3461,8 @@ "node_modules/csv-parse": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", - "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==" + "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", + "license": "MIT" }, "node_modules/death": { "version": "1.1.0", @@ -6068,20 +6067,71 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "license": "MIT", + "peer": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-solidity": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-2.1.0.tgz", + "integrity": "sha512-O5HX4/PCE5aqiaEiNGbSRLbSBZQ6kLswAav5LBSewwzhT+sZlN6iAaLZlZcJzPEnIAxwLEHP03xKEg92fflT9Q==", + "license": "MIT", + "dependencies": { + "@nomicfoundation/slang": "1.2.0", + "@solidity-parser/parser": "^0.20.1", + "semver": "^7.7.2" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "prettier": ">=3.0.0" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.2.tgz", + "integrity": "sha512-mNm/lblgES8UkVle8rGImXOz4TtL3eU3inHay/7TVchkKrb/lgcVvTK0+VAw8p5zQ0rgQsXm1j5dOlAAd+MeoA==", + "license": "(Apache-2.0 WITH LLVM-exception)" + }, + "node_modules/prettier-plugin-solidity/node_modules/@nomicfoundation/slang": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-1.2.0.tgz", + "integrity": "sha512-+04Z1RHbbz0ldDbHKQFOzveCdI9Rd3TZZu7fno5hHy3OsqTo9UK5Jgqo68wMvRovCO99POv6oCEyO7+urGeN8Q==", + "license": "MIT", + "dependencies": { + "@bytecodealliance/preview2-shim": "0.17.2" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/@solidity-parser/parser": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.20.1.tgz", + "integrity": "sha512-58I2sRpzaQUN+jJmWbHfbWf9AKfzqCI8JAdFB0vbyY+u8tBRcuTt9LxzasvR0LGQpcRv97eyV7l61FQ3Ib7zVw==", + "license": "MIT" + }, + "node_modules/prettier-plugin-solidity/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -7597,6 +7647,22 @@ "node": ">=10" } }, + "node_modules/typechain/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/package.json b/package.json index 56cdab0..dc333e0 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "typescript": "^5.5.4" }, "dependencies": { - "csv-parse": "^5.6.0" + "csv-parse": "^5.6.0", + "prettier-plugin-solidity": "^2.1.0" } } diff --git a/scripts/.python-version b/scripts/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/scripts/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/scripts/deployRewardNotifier.ts b/scripts/deployRewardNotifier.ts new file mode 100644 index 0000000..6bcc4d4 --- /dev/null +++ b/scripts/deployRewardNotifier.ts @@ -0,0 +1,38 @@ +import { ethers, run } from "hardhat" + +async function main() { + const symmAddress = "0x800822d361335b4d5F352Dac293cA4128b5B605f" + const admin = "0x5146C35725d9b8F11A84ebD4a3abe9845698Ada9" + const stakingAddress = "0x573310A15f3dc4828994819bc67AB6B1596AC90c" + + const [deployer] = await ethers.getSigners() + + console.log("Deploying contracts with the account:", deployer.address) + + // Deploy MultiAccount as upgradeable + const Factory = await ethers.getContractFactory("RewardNotifier") + const contract = await Factory.deploy(admin, symmAddress, stakingAddress) + await contract.waitForDeployment() + + const deployedAddress = await contract.getAddress() + console.log("RewardNotifier deployed to", deployedAddress) + + await new Promise(resolve => setTimeout(resolve, 15000)) + + try { + console.log(`Verifying ${deployedAddress}`) + await run("verify:verify", { + address: deployedAddress, + constructorArguments: [admin, symmAddress, stakingAddress], + }) + } catch (err) { + console.error(err) + } +} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/deployVestingUpsertManager.ts b/scripts/deployVestingUpsertManager.ts new file mode 100644 index 0000000..c3a4d26 --- /dev/null +++ b/scripts/deployVestingUpsertManager.ts @@ -0,0 +1,28 @@ +const { ethers, upgrades } = require("hardhat") + +async function main() { + const [deployer] = await ethers.getSigners() + + console.log("Deploying with account:", deployer.address) + + const admin = "" + const operator = "" + const vestingAddress = "0x5733105364c8136226e246455328884c23151C60" + const vestingPlanAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15" + + const Factory = await ethers.getContractFactory("VestingUpsertManager") + const contract = await upgrades.deployProxy(Factory, [admin, operator, vestingAddress, vestingPlanAddress], { initializer: "initialize" }) + await contract.waitForDeployment() + + const addresses = { + proxy: await contract.getAddress(), + admin: await upgrades.erc1967.getAdminAddress(await contract.getAddress()), + implementation: await upgrades.erc1967.getImplementationAddress(await contract.getAddress()), + } + console.log("VestingUpsertManager deployed to", addresses) +} + +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) diff --git a/scripts/py/future_reset_vesting_plans.py b/scripts/py/future_reset_vesting_plans.py new file mode 100644 index 0000000..0da745d --- /dev/null +++ b/scripts/py/future_reset_vesting_plans.py @@ -0,0 +1,498 @@ +import csv +import json +import logging +import sys +from datetime import datetime +from multicallable import Multicallable +from web3 import Web3 + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("vesting_script.log"), + logging.StreamHandler(sys.stdout), + ], +) +logger = logging.getLogger(__name__) + +# Configuration +TOKEN_ADDRESS = "" +VESTING_CONTRACT = "" +PENDING_CONTRACT = "" # New contract for pending amounts +NEW_START_TIME = 1749996000 +NEW_END_TIME = 1758386880 +EXECUTION_TIMESTAMP = 1749996000 +RPC_URL = "https://base.drpc.org" +BATCH_SIZE = 150 # Batch size for setup transactions + +# Initialize Web3 +logger.info("Initializing Web3 connection...") +w3 = Web3(Web3.HTTPProvider(RPC_URL)) + +# Check connection +if w3.is_connected(): + logger.info(f"Successfully connected to {RPC_URL}") + logger.info(f"Latest block: {w3.eth.block_number}") +else: + logger.error(f"Failed to connect to {RPC_URL}") + sys.exit(1) + +# Log configuration +logger.info("=== Configuration ===") +logger.info(f"Token Address: {TOKEN_ADDRESS}") +logger.info(f"Vesting Contract: {VESTING_CONTRACT}") +logger.info(f"Pending Contract: {PENDING_CONTRACT}") +logger.info( + f"New Start Time: {NEW_START_TIME} ({datetime.fromtimestamp(NEW_START_TIME)})" +) +logger.info(f"New End Time: {NEW_END_TIME} ({datetime.fromtimestamp(NEW_END_TIME)})") +logger.info( + f"Execution Timestamp: {EXECUTION_TIMESTAMP} ({datetime.fromtimestamp(EXECUTION_TIMESTAMP)})" +) +logger.info(f"Batch Size for Setup Transactions: {BATCH_SIZE}") + +# Minimal ABI for vesting contract +VESTING_ABI = [ + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"}, + ], + "name": "vestingPlans", + "outputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + {"internalType": "uint256", "name": "claimedAmount", "type": "uint256"}, + {"internalType": "uint256", "name": "startTime", "type": "uint256"}, + {"internalType": "uint256", "name": "endTime", "type": "uint256"}, + ], + "stateMutability": "view", + "type": "function", + } +] + +# ABI for pending amounts contract +PENDING_ABI = [ + { + "inputs": [ + {"internalType": "address", "name": "user", "type": "address"}, + ], + "name": "pendingAmount", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"}, + ], + "stateMutability": "view", + "type": "function", + } +] + + +def calculate_future_locked_amount(vesting_plan, future_timestamp): + """Calculate the locked amount at a future timestamp.""" + amount, claimed_amount, start_time, end_time = vesting_plan + + logger.debug( + f"Calculating future locked amount for plan: amount={amount}, claimed={claimed_amount}, start={start_time}, end={end_time}" + ) + + if amount == 0: + logger.debug("Plan has zero amount, returning 0") + return 0 + + # Calculate unlocked amount at future timestamp + if future_timestamp >= end_time: + unlocked = amount + logger.debug(f"Future timestamp >= end time, fully unlocked: {unlocked}") + elif future_timestamp <= start_time: + unlocked = 0 + logger.debug(f"Future timestamp <= start time, nothing unlocked: {unlocked}") + else: + duration = end_time - start_time + elapsed = future_timestamp - start_time + unlocked = (amount * elapsed) // duration + logger.debug( + f"Partial unlock: duration={duration}, elapsed={elapsed}, unlocked={unlocked}" + ) + + locked = amount - unlocked + logger.debug(f"Final locked amount: {locked}") + return locked + + +def batch_list(input_list, batch_size): + """Split a list into batches of specified size.""" + for i in range(0, len(input_list), batch_size): + yield input_list[i : i + batch_size] + + +def main(): + logger.info("=== Starting Vesting Plan Setup ===") + + # Initialize multicallable contracts + logger.info("Initializing multicallable contracts...") + try: + vesting_contract = Multicallable(VESTING_CONTRACT, VESTING_ABI, w3) + pending_contract = Multicallable(PENDING_CONTRACT, PENDING_ABI, w3) + logger.info("Multicallable contracts initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize multicallable contracts: {e}") + return + + # Read CSV + logger.info("Reading CSV file...") + users_data = [] + try: + with open("users_amounts.csv", "r") as file: + reader = csv.reader(file) + header = next(reader) # Skip header + logger.info(f"CSV header: {header}") + + row_count = 0 + for row in reader: + try: + user = w3.to_checksum_address(row[0]) + amount = int(float(row[1])) # Convert to float first, then to int + users_data.append((user, amount)) + row_count += 1 + + if row_count <= 5: # Log first 5 entries for verification + logger.info(f"Row {row_count}: User={user}, Amount={amount}") + elif row_count % 50 == 0: # Log every 50th entry + logger.info(f"Processed {row_count} rows...") + + except Exception as e: + logger.error(f"Error processing row {row_count + 1}: {row} - {e}") + continue + + logger.info(f"Successfully read {len(users_data)} user records from CSV") + + except FileNotFoundError: + logger.error("CSV file 'users_amounts.csv' not found") + return + except Exception as e: + logger.error(f"Error reading CSV file: {e}") + return + + if not users_data: + logger.error("No valid user data found in CSV") + return + + # Extract users for batch call + all_users = [user for user, _ in users_data] + logger.info(f"Prepared {len(all_users)} users for batch vesting plan fetch") + + # Batch fetch all vesting plans + logger.info("Fetching existing vesting plans...") + + try: + # Create multicall parameters for vesting plans + vesting_call_params = [(TOKEN_ADDRESS, user) for user in all_users] + logger.info(f"Created {len(vesting_call_params)} vesting multicall parameters") + + # Calculate batch size + batch_size = len(all_users) // 100 + 1 + logger.info(f"Using batch size: {batch_size}") + + # Execute batch call for vesting plans + logger.info("Executing vesting plans multicall...") + vesting_results = vesting_contract.vestingPlans(vesting_call_params).call( + n=batch_size, progress_bar=True + ) + logger.info(f"Successfully fetched {len(vesting_results)} vesting plans") + + except Exception as e: + logger.error(f"Error during vesting multicall execution: {e}") + return + + # Identify users without vesting plans + no_vesting_users = [] + for i, (user, _) in enumerate(users_data): + if vesting_results[i][0] == 0: # No existing plan + no_vesting_users.append(user) + + # Batch fetch pending amounts for users without vesting plans + pending_amounts = {} + if no_vesting_users: + logger.info( + f"Fetching pending amounts for {len(no_vesting_users)} users without vesting plans..." + ) + try: + # Create multicall parameters for pending amounts + pending_call_params = no_vesting_users + + # Execute batch call for pending amounts + logger.info("Executing pending amounts multicall...") + pending_results = pending_contract.pendingAmount(pending_call_params).call( + n=len(no_vesting_users) // 100 + 1, progress_bar=True + ) + + # Map results to users + for i, user in enumerate(no_vesting_users): + pending_amounts[user] = pending_results[i] + if pending_results[i] > 0: + logger.info( + f"User {user} has pending amount: {pending_results[i] / 1e18:,}" + ) + + logger.info( + f"Successfully fetched pending amounts for {len(no_vesting_users)} users" + ) + + except Exception as e: + logger.error(f"Error during pending amounts multicall execution: {e}") + # Initialize with zeros if fetch fails + for user in no_vesting_users: + pending_amounts[user] = 0 + + # Process results + logger.info("Processing results...") + setup_users = [] + setup_amounts = [] + reset_users = [] + reset_amounts = [] + + existing_plans_count = 0 + new_plans_count = 0 + total_new_vested_amount = 0 + total_setup_new_amount = 0 + total_reset_new_amount = 0 + total_pending_amount = 0 + + logger.info("=== Individual Address Processing ===") + + for i, (user, csv_amount) in enumerate(users_data): + try: + plan = vesting_results[i] + user_index = i + 1 + + total_new_vested_amount += csv_amount + + if plan[0] == 0: # No existing plan + # Check for pending amount + pending = pending_amounts.get(user, 0) + total_amount = csv_amount + pending + + setup_users.append(user) + setup_amounts.append(total_amount) + new_plans_count += 1 + total_setup_new_amount += csv_amount + total_pending_amount += pending + + if pending > 0: + logger.info( + f"[{user_index:3d}/{len(users_data)}] NEW - {user} | CSV Amount: {csv_amount / 1e18:,} | Pending: {pending / 1e18:,} | Total: {total_amount / 1e18:,} | Action: Setup" + ) + else: + logger.info( + f"[{user_index:3d}/{len(users_data)}] NEW - {user} | Amount: {csv_amount / 1e18:,} | Action: Setup" + ) + + else: + existing_plans_count += 1 + + # Extract existing plan details + existing_amount, claimed_amount, start_time, end_time = plan + + # Calculate future locked amount + future_locked = calculate_future_locked_amount( + plan, EXECUTION_TIMESTAMP + ) + total_amount = future_locked + csv_amount + + reset_users.append(user) + reset_amounts.append(total_amount) + total_reset_new_amount += csv_amount + + claimed_pct = ( + (claimed_amount / existing_amount * 100) + if existing_amount > 0 + else 0 + ) + + logger.info( + f"[{user_index:3d}/{len(users_data)}] RESET - {user} | Existing: {existing_amount / 1e18:,} | Claimed: {claimed_amount / 1e18:,} ({claimed_pct:.1f}%) | Future Locked: {future_locked / 1e18:,} | New: {csv_amount / 1e18:,} | Total: {total_amount / 1e18:,}" + ) + + except Exception as e: + logger.error(f"[{user_index:3d}/{len(users_data)}] ERROR - {user}: {e}") + continue + + logger.info("=== Processing Summary ===") + logger.info(f"Total users processed: {len(users_data)}") + logger.info(f"Users with existing plans: {existing_plans_count}") + logger.info(f"Users needing new plans: {new_plans_count}") + logger.info(f"Users for setup transaction: {len(setup_users)}") + logger.info(f"Users for reset transaction: {len(reset_users)}") + logger.info("") + logger.info("=== Vesting Amount Summary ===") + logger.info( + f"Total NEW vested amount (setup users): {total_setup_new_amount / 1e18:,}" + ) + logger.info( + f"Total NEW vested amount (reset users): {total_reset_new_amount / 1e18:,}" + ) + logger.info( + f"Total NEW vested amount (all users): {total_new_vested_amount / 1e18:,}" + ) + logger.info(f"Total pending amount included: {total_pending_amount / 1e18:,}") + logger.info(f"Total transaction amount (setup): {sum(setup_amounts) / 1e18:,}") + logger.info(f"Total transaction amount (reset): {sum(reset_amounts) / 1e18:,}") + + # Generate calldata using web3py (since multicallable is for reading) + logger.info("Generating transaction calldata...") + + try: + vesting_interface = w3.eth.contract( + address=VESTING_CONTRACT, + abi=[ + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"}, + { + "internalType": "uint256", + "name": "startTime", + "type": "uint256", + }, + { + "internalType": "uint256", + "name": "endTime", + "type": "uint256", + }, + { + "internalType": "address[]", + "name": "users", + "type": "address[]", + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]", + }, + ], + "name": "setupVestingPlans", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"}, + { + "internalType": "address[]", + "name": "users", + "type": "address[]", + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]", + }, + ], + "name": "resetVestingPlans", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + ], + ) + + # Transaction(s) for setupVestingPlans - now in batches + if setup_users: + logger.info( + f"Generating setupVestingPlans transactions in batches of {BATCH_SIZE}..." + ) + + # Calculate number of batches + num_batches = (len(setup_users) + BATCH_SIZE - 1) // BATCH_SIZE + logger.info(f"Will create {num_batches} setupVestingPlans transactions") + + # Create batches + user_batches = list(batch_list(setup_users, BATCH_SIZE)) + amount_batches = list(batch_list(setup_amounts, BATCH_SIZE)) + + for batch_index, (batch_users, batch_amounts) in enumerate( + zip(user_batches, amount_batches) + ): + batch_num = batch_index + 1 + batch_total_amount = sum(batch_amounts) / 1e18 + + setup_calldata = vesting_interface.encode_abi( + "setupVestingPlans", + args=[ + TOKEN_ADDRESS, + NEW_START_TIME, + NEW_END_TIME, + batch_users, + batch_amounts, + ], + ) + + logger.info( + f"\n=== Transaction {batch_num}: setupVestingPlans (Batch {batch_num}/{num_batches}) ===" + ) + logger.info(f"To: {VESTING_CONTRACT}") + logger.info(f"Users count: {len(batch_users)}") + logger.info(f"Batch total amount: {batch_total_amount:,}") + logger.info(f"Calldata length: {len(setup_calldata)} bytes") + print(f"\nBatch {batch_num} Calldata: {setup_calldata}") + + # Optionally save each batch calldata to a separate file + with open(f"setup_batch_{batch_num}_calldata.txt", "w") as f: + f.write(f"To: {VESTING_CONTRACT}\n") + f.write(f"Users count: {len(batch_users)}\n") + f.write(f"Batch total amount: {batch_total_amount:,}\n") + f.write(f"Calldata: {setup_calldata}\n") + logger.info( + f"Saved batch {batch_num} calldata to setup_batch_{batch_num}_calldata.txt" + ) + + else: + logger.info("No users require setupVestingPlans transaction") + + # Transaction for resetVestingPlans (keeping as single transaction) + if reset_users: + logger.info("\nGenerating resetVestingPlans transaction...") + reset_calldata = vesting_interface.encode_abi( + "resetVestingPlans", args=[TOKEN_ADDRESS, reset_users, reset_amounts] + ) + + logger.info("\n=== Reset Transaction: resetVestingPlans ===") + logger.info(f"To: {VESTING_CONTRACT}") + logger.info(f"Users count: {len(reset_users)}") + logger.info(f"Total amount: {sum(reset_amounts) / 1e18:,}") + logger.info(f"Calldata length: {len(reset_calldata)} bytes") + print(f"\nReset Calldata: {reset_calldata}") + + # Save reset calldata to file + with open("reset_calldata.txt", "w") as f: + f.write(f"To: {VESTING_CONTRACT}\n") + f.write(f"Users count: {len(reset_users)}\n") + f.write(f"Total amount: {sum(reset_amounts) / 1e18:,}\n") + f.write(f"Calldata: {reset_calldata}\n") + logger.info("Saved reset calldata to reset_calldata.txt") + else: + logger.info("No users require resetVestingPlans transaction") + + except Exception as e: + logger.error(f"Error generating transaction calldata: {e}") + return + + logger.info("\n=== Script completed successfully ===") + + # Final summary + if setup_users: + logger.info(f"\nSetup transactions summary:") + logger.info(f"- Total setup users: {len(setup_users)}") + logger.info(f"- Number of batches: {num_batches}") + logger.info(f"- Batch size: {BATCH_SIZE}") + logger.info(f"- Last batch size: {len(user_batches[-1])}") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + logger.info("Script interrupted by user") + except Exception as e: + logger.error(f"Unexpected error: {e}", exc_info=True) diff --git a/scripts/py/reset_vesting_plans.py b/scripts/py/reset_vesting_plans.py new file mode 100644 index 0000000..f33dd5b --- /dev/null +++ b/scripts/py/reset_vesting_plans.py @@ -0,0 +1,250 @@ +import json +import os +import csv +from typing import List, Tuple +from multicallable import Multicallable +from web3 import Web3 +from eth_account import Account +import time + +# Configuration +RPC_URL = "https://base.drpc.org" # You can change this to your preferred RPC +VESTING_CONTRACT_ADDRESS = "" # Replace with your deployed contract address +TOKEN_ADDRESS = "" # Replace with the token address for vesting +PRIVATE_KEY = "" # Replace with your private key (keep this secure!) + +# CSV Input Format: +# The CSV file should have two columns: 'user' and 'amount' +# Example: +# user,amount +# 0x1234567890123456789012345678901234567890,1000000000000000000 +# 0x2345678901234567890123456789012345678901,2000000000000000000 + +# Initialize Web3 +w3 = Web3(Web3.HTTPProvider(RPC_URL)) +account = Account.from_key(PRIVATE_KEY) + +# Vesting contract ABI (minimal ABI with just the functions we need) +VESTING_ABI = [ + { + "inputs": [ + {"internalType": "address", "name": "user", "type": "address"}, + {"internalType": "address", "name": "token", "type": "address"} + ], + "name": "getLockedAmountsForToken", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"}, + {"internalType": "address[]", "name": "users", "type": "address[]"}, + {"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"} + ], + "name": "resetVestingPlans", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] + +def load_user_updates(filename: str) -> List[Tuple[str, int]]: + """ + Load user addresses and amounts to add from a CSV file. + Expected format: CSV with columns 'user' and 'amount' + """ + import csv + + user_updates = [] + + with open(filename, 'r') as f: + reader = csv.DictReader(f) + + # Check if required columns exist + if 'user' not in reader.fieldnames or 'amount' not in reader.fieldnames: + raise ValueError("CSV must have 'user' and 'amount' columns") + + for row in reader: + user = w3.to_checksum_address(row['user'].strip()) + amount = int(row['amount'].strip()) + user_updates.append((user, amount)) + + print(f"Loaded {len(user_updates)} user updates from CSV") + return user_updates + +def get_current_locked_amounts(users: List[str], token: str) -> List[int]: + """ + Get current locked amounts for all users using multicallable + """ + print(f"Fetching current locked amounts for {len(users)} users...") + + # Create multicallable contract instance + contract = Multicallable( + w3.to_checksum_address(VESTING_CONTRACT_ADDRESS), + VESTING_ABI, + w3 + ) + + # Prepare calls for getLockedAmountsForToken + # We need to pass both user and token for each call + calls = [] + for user in users: + calls.append((user, token)) + + # Execute multicall + locked_amounts = contract.getLockedAmountsForToken(calls).call( + n=len(users) // 200 + 1, # Split into chunks of 200 + progress_bar=True + ) + + return locked_amounts + +def prepare_update_data(user_updates: List[Tuple[str, int]], current_locked: List[int]) -> List[Tuple[str, int]]: + """ + Combine current locked amounts with amounts to add + """ + updated_data = [] + + for i, (user, amount_to_add) in enumerate(user_updates): + current_amount = current_locked[i] + new_amount = current_amount + amount_to_add + updated_data.append((user, new_amount)) + print(f"User {user}: {current_amount} + {amount_to_add} = {new_amount}") + + return updated_data + +def send_batch_transaction(token: str, users: List[str], amounts: List[int], batch_num: int) -> str: + """ + Send a transaction to reset vesting plans for a batch of users + """ + contract = w3.eth.contract( + address=w3.to_checksum_address(VESTING_CONTRACT_ADDRESS), + abi=VESTING_ABI + ) + + # Build transaction + nonce = w3.eth.get_transaction_count(account.address) + + tx = contract.functions.resetVestingPlans( + token, + users, + amounts + ).build_transaction({ + 'from': account.address, + 'nonce': nonce, + 'gas': 5000000, # Adjust based on your needs + 'gasPrice': w3.eth.gas_price, + 'chainId': w3.eth.chain_id + }) + + # Sign and send transaction + signed_tx = account.sign_transaction(tx) + tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) + + print(f"Batch {batch_num} transaction sent: {tx_hash.hex()}") + + # Wait for confirmation + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt['status'] == 1: + print(f"Batch {batch_num} transaction successful!") + else: + print(f"Batch {batch_num} transaction failed!") + raise Exception(f"Transaction failed for batch {batch_num}") + + return tx_hash.hex() + +def update_vesting_plans(input_file: str, batch_size: int = 300): + """ + Main function to update vesting plans from a CSV file + """ + # Load user updates + user_updates = load_user_updates(input_file) + print(f"Loaded {len(user_updates)} user updates") + + # Extract users list + users = [user for user, _ in user_updates] + + # Get current locked amounts + current_locked = get_current_locked_amounts(users, TOKEN_ADDRESS) + + # Prepare updated data + updated_data = prepare_update_data(user_updates, current_locked) + + # Split into batches and send transactions + total_batches = (len(updated_data) + batch_size - 1) // batch_size + print(f"\nProcessing {total_batches} batches of up to {batch_size} users each...") + + transaction_hashes = [] + + for batch_num in range(total_batches): + start_idx = batch_num * batch_size + end_idx = min((batch_num + 1) * batch_size, len(updated_data)) + + batch_data = updated_data[start_idx:end_idx] + batch_users = [user for user, _ in batch_data] + batch_amounts = [amount for _, amount in batch_data] + + print(f"\nProcessing batch {batch_num + 1}/{total_batches} ({len(batch_data)} users)...") + + try: + tx_hash = send_batch_transaction(TOKEN_ADDRESS, batch_users, batch_amounts, batch_num + 1) + transaction_hashes.append(tx_hash) + + # Wait a bit between transactions to avoid nonce issues + if batch_num < total_batches - 1: + time.sleep(2) + + except Exception as e: + print(f"Error processing batch {batch_num + 1}: {e}") + # Save progress + save_progress(transaction_hashes, batch_num) + raise + + # Save all transaction hashes + save_progress(transaction_hashes, total_batches) + print(f"\nAll batches processed successfully! Total transactions: {len(transaction_hashes)}") + +def save_progress(transaction_hashes: List[str], batches_processed: int): + """ + Save transaction hashes to a file for record keeping + """ + output_file = f"vesting_update_txs_{int(time.time())}.json" + with open(output_file, 'w') as f: + json.dump({ + 'transaction_hashes': transaction_hashes, + 'batches_processed': batches_processed, + 'timestamp': int(time.time()) + }, f, indent=2) + print(f"Progress saved to {output_file}") + +def create_sample_input_file(): + """ + Create a sample CSV input file for testing + """ + import csv + + sample_data = [ + {"user": "0x1234567890123456789012345678901234567890", "amount": "1000000000000000000"}, + {"user": "0x2345678901234567890123456789012345678901", "amount": "2000000000000000000"}, + {"user": "0x3456789012345678901234567890123456789012", "amount": "1500000000000000000"}, + # Add more users here + ] + + with open("vesting_updates_input.csv", 'w', newline='') as f: + fieldnames = ['user', 'amount'] + writer = csv.DictWriter(f, fieldnames=fieldnames) + + writer.writeheader() + writer.writerows(sample_data) + + print("Sample CSV input file created: vesting_updates_input.csv") + +if __name__ == "__main__": + # Uncomment to create a sample input file + create_sample_input_file() + + # Run the update + input_file = "vesting_updates_input.csv" # Your CSV input file + update_vesting_plans(input_file, batch_size=300) \ No newline at end of file diff --git a/scripts/py/vesting_setup.py b/scripts/py/vesting_setup.py new file mode 100644 index 0000000..ed4373c --- /dev/null +++ b/scripts/py/vesting_setup.py @@ -0,0 +1,641 @@ +""" +Vesting Plan Update Script + +This script updates vesting plans by adding specified amounts to existing locked amounts. +For users without existing plans, it creates new vesting plans. + +Usage: + python update_vesting_plans.py # Run in live mode (sends transactions) + python update_vesting_plans.py --dry-run # Run in dry mode (no transactions) + +Dry run mode will: +- Calculate all changes +- Show what would be updated +- Save results to files for review +- NOT send any transactions +""" + +import json +import os +import csv +import sys +from typing import List, Tuple +from multicallable import Multicallable +from web3 import Web3 +from eth_account import Account +import time + +# Configuration +RPC_URL = "https://base.drpc.org" # You can change this to your preferred RPC +VESTING_CONTRACT_ADDRESS = "" # Replace with your deployed contract address +TOKEN_ADDRESS = "" # Replace with the token address for vesting +PRIVATE_KEY = "" # Replace with your private key (keep this secure!) + +# Vesting schedule configuration for new plans +VESTING_START_TIME = int(time.time()) # Default: current time +VESTING_END_TIME = int(time.time()) + (365 * 24 * 60 * 60) # Default: 1 year from now + +# CSV Input Format: +# The CSV file should have two columns: 'user' and 'amount' +# Example: +# user,amount +# 0x1234567890123456789012345678901234567890,1000000000000000000 +# 0x2345678901234567890123456789012345678901,2000000000000000000 + +# Dry Run Mode: +# Use --dry-run flag to simulate the update without sending transactions +# This will create two output files: +# 1. dry_run_batches_.json - Detailed batch information +# 2. dry_run_summary_.csv - Summary of all updates + +# Initialize Web3 +w3 = Web3(Web3.HTTPProvider(RPC_URL)) + +# Only initialize account if not in dry run mode +account = None +if PRIVATE_KEY != "YOUR_PRIVATE_KEY": + account = Account.from_key(PRIVATE_KEY) + +# Vesting contract ABI (minimal ABI with just the functions we need) +VESTING_ABI = [ + { + "inputs": [ + {"internalType": "address", "name": "user", "type": "address"}, + {"internalType": "address", "name": "token", "type": "address"}, + ], + "name": "getLockedAmountsForToken", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"}, + {"internalType": "address", "name": "", "type": "address"}, + ], + "name": "vestingPlans", + "outputs": [ + {"internalType": "uint256", "name": "totalAmount", "type": "uint256"}, + {"internalType": "uint256", "name": "claimedAmount", "type": "uint256"}, + {"internalType": "uint256", "name": "startTime", "type": "uint256"}, + {"internalType": "uint256", "name": "endTime", "type": "uint256"}, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"}, + {"internalType": "address[]", "name": "users", "type": "address[]"}, + {"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}, + ], + "name": "resetVestingPlans", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"}, + {"internalType": "uint256", "name": "startTime", "type": "uint256"}, + {"internalType": "uint256", "name": "endTime", "type": "uint256"}, + {"internalType": "address[]", "name": "users", "type": "address[]"}, + {"internalType": "uint256[]", "name": "amounts", "type": "uint256[]"}, + ], + "name": "setupVestingPlans", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, +] + + +def load_user_updates(filename: str) -> List[Tuple[str, int]]: + """ + Load user addresses and amounts to add from a CSV file. + Expected format: CSV with columns 'user' and 'amount' + """ + import csv + + user_updates = [] + + with open(filename, "r") as f: + reader = csv.DictReader(f) + + # Check if required columns exist + if "user" not in reader.fieldnames or "amount" not in reader.fieldnames: + raise ValueError("CSV must have 'user' and 'amount' columns") + + for row in reader: + user = w3.to_checksum_address(row["user"].strip()) + amount = int(row["amount"].strip()) + user_updates.append((user, amount)) + + print(f"Loaded {len(user_updates)} user updates from CSV") + return user_updates + + +def get_vesting_plan_info(users: List[str], token: str) -> List[Tuple]: + """ + Get vesting plan information for all users + Returns list of tuples: (totalAmount, claimedAmount, startTime, endTime) + """ + print(f"Fetching vesting plan information for {len(users)} users...") + + # Create multicallable contract instance + contract = Multicallable( + w3.to_checksum_address(VESTING_CONTRACT_ADDRESS), VESTING_ABI, w3 + ) + + # Prepare calls for vestingPlans + calls = [] + for user in users: + calls.append((token, user)) + + # Execute multicall + vesting_info = contract.vestingPlans(calls).call( + n=len(users) // 200 + 1, # Split into chunks of 200 + progress_bar=True, + ) + + return vesting_info + + +def categorize_users( + user_updates: List[Tuple[str, int]], vesting_info: List[Tuple] +) -> Tuple[List[Tuple[str, int]], List[Tuple[str, int]]]: + """ + Categorize users into those with existing plans and those without + Returns: (existing_users, new_users) + """ + existing_users = [] + new_users = [] + + for i, (user, amount) in enumerate(user_updates): + total_amount = vesting_info[i][0] # totalAmount from vestingPlans + + if total_amount > 0: + # User has an existing vesting plan + existing_users.append((user, amount)) + else: + # User doesn't have a vesting plan + new_users.append((user, amount)) + + return existing_users, new_users + + +def prepare_update_data( + user_updates: List[Tuple[str, int]], + vesting_info: List[Tuple], + dry_run: bool = False, +) -> Tuple[List[Tuple[str, int]], List[Tuple[str, int]]]: + """ + Prepare update data for both existing and new vesting plans + For existing plans: add amounts to current locked amounts + For new plans: use the input amounts directly + Returns: (existing_updates, new_setups) + """ + existing_updates = [] + new_setups = [] + + if dry_run: + print("\nCalculating updates:") + print("-" * 100) + print( + f"{'User Address':<42} {'Status':<15} {'Current':<20} {'To Add':<20} {'New Total':<20}" + ) + print("-" * 100) + + total_current = 0 + total_to_add = 0 + total_new = 0 + + for i, (user, amount_to_add) in enumerate(user_updates): + total_amount = vesting_info[i][0] + claimed_amount = vesting_info[i][1] + + if total_amount > 0: + # Existing vesting plan - calculate current locked and add new amount + current_locked = total_amount - claimed_amount + new_amount = current_locked + amount_to_add + existing_updates.append((user, new_amount)) + status = "EXISTING" + + if dry_run: + print( + f"{user} {status:<15} {current_locked:<20} {amount_to_add:<20} {new_amount:<20}" + ) + total_current += current_locked + total_to_add += amount_to_add + total_new += new_amount + else: + # No vesting plan - use input amount directly + new_setups.append((user, amount_to_add)) + status = "NEW" + + if dry_run: + print( + f"{user} {status:<15} {'0':<20} {amount_to_add:<20} {amount_to_add:<20}" + ) + total_to_add += amount_to_add + total_new += amount_to_add + + if dry_run: + print("-" * 100) + print( + f"{'TOTALS:':<42} {'':<15} {total_current:<20} {total_to_add:<20} {total_new:<20}" + ) + print("-" * 100) + print(f"Existing vesting plans to update: {len(existing_updates)}") + print(f"New vesting plans to create: {len(new_setups)}") + print(f"Total users: {len(user_updates)}") + + return existing_updates, new_setups + + +def send_reset_batch_transaction( + token: str, users: List[str], amounts: List[int], batch_num: int +) -> str: + """ + Send a transaction to reset vesting plans for a batch of users + """ + if not account: + raise ValueError( + "Account not initialized. Please set PRIVATE_KEY in the configuration." + ) + + contract = w3.eth.contract( + address=w3.to_checksum_address(VESTING_CONTRACT_ADDRESS), abi=VESTING_ABI + ) + + # Build transaction + nonce = w3.eth.get_transaction_count(account.address) + + tx = contract.functions.resetVestingPlans(token, users, amounts).build_transaction( + { + "from": account.address, + "nonce": nonce, + "gas": 5000000, # Adjust based on your needs + "gasPrice": w3.eth.gas_price, + "chainId": w3.eth.chain_id, + } + ) + + # Sign and send transaction + signed_tx = account.sign_transaction(tx) + tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) + + print(f"Reset batch {batch_num} transaction sent: {tx_hash.hex()}") + + # Wait for confirmation + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt["status"] == 1: + print(f"Reset batch {batch_num} transaction successful!") + else: + print(f"Reset batch {batch_num} transaction failed!") + raise Exception(f"Transaction failed for reset batch {batch_num}") + + return tx_hash.hex() + + +def send_setup_batch_transaction( + token: str, users: List[str], amounts: List[int], batch_num: int +) -> str: + """ + Send a transaction to setup new vesting plans for a batch of users + """ + if not account: + raise ValueError( + "Account not initialized. Please set PRIVATE_KEY in the configuration." + ) + + contract = w3.eth.contract( + address=w3.to_checksum_address(VESTING_CONTRACT_ADDRESS), abi=VESTING_ABI + ) + + # Build transaction + nonce = w3.eth.get_transaction_count(account.address) + + tx = contract.functions.setupVestingPlans( + token, VESTING_START_TIME, VESTING_END_TIME, users, amounts + ).build_transaction( + { + "from": account.address, + "nonce": nonce, + "gas": 5000000, # Adjust based on your needs + "gasPrice": w3.eth.gas_price, + "chainId": w3.eth.chain_id, + } + ) + + # Sign and send transaction + signed_tx = account.sign_transaction(tx) + tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction) + + print(f"Setup batch {batch_num} transaction sent: {tx_hash.hex()}") + + # Wait for confirmation + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + + if receipt["status"] == 1: + print(f"Setup batch {batch_num} transaction successful!") + else: + print(f"Setup batch {batch_num} transaction failed!") + raise Exception(f"Transaction failed for setup batch {batch_num}") + + return tx_hash.hex() + + +def update_vesting_plans(input_file: str, batch_size: int = 300, dry_run: bool = False): + """ + Main function to update vesting plans from a CSV file + + Args: + input_file: Path to CSV file with user addresses and amounts + batch_size: Number of users to process in each transaction + dry_run: If True, only calculate and display changes without sending transactions + """ + # Validate configuration + if VESTING_CONTRACT_ADDRESS == "YOUR_VESTING_CONTRACT_ADDRESS": + raise ValueError("Please set VESTING_CONTRACT_ADDRESS in the configuration") + if TOKEN_ADDRESS == "YOUR_TOKEN_ADDRESS": + raise ValueError("Please set TOKEN_ADDRESS in the configuration") + if not dry_run and PRIVATE_KEY == "YOUR_PRIVATE_KEY": + raise ValueError("Please set PRIVATE_KEY in the configuration for live mode") + + # Load user updates + user_updates = load_user_updates(input_file) + print(f"Loaded {len(user_updates)} user updates") + + # Extract users list + users = [user for user, _ in user_updates] + + # Get vesting plan information for all users + vesting_info = get_vesting_plan_info(users, TOKEN_ADDRESS) + + # Prepare updated data, separating existing and new users + existing_updates, new_setups = prepare_update_data( + user_updates, vesting_info, dry_run + ) + + # Process batches for both existing and new users + all_transaction_hashes = [] + all_batch_data = [] + + # Process existing user updates (resetVestingPlans) + if existing_updates: + print(f"\n{'=' * 50}") + print(f"Processing {len(existing_updates)} existing vesting plan updates...") + print(f"{'=' * 50}") + + total_reset_batches = (len(existing_updates) + batch_size - 1) // batch_size + + for batch_num in range(total_reset_batches): + start_idx = batch_num * batch_size + end_idx = min((batch_num + 1) * batch_size, len(existing_updates)) + + batch_data = existing_updates[start_idx:end_idx] + batch_users = [user for user, _ in batch_data] + batch_amounts = [amount for _, amount in batch_data] + + print( + f"\nProcessing RESET batch {batch_num + 1}/{total_reset_batches} ({len(batch_data)} users)..." + ) + + if dry_run: + batch_info = { + "operation": "reset", + "batch_number": batch_num + 1, + "users": batch_users, + "amounts": batch_amounts, + "user_updates": batch_data, + } + all_batch_data.append(batch_info) + else: + try: + tx_hash = send_reset_batch_transaction( + TOKEN_ADDRESS, batch_users, batch_amounts, batch_num + 1 + ) + all_transaction_hashes.append(("reset", tx_hash)) + time.sleep(2) + except Exception as e: + print(f"Error processing reset batch {batch_num + 1}: {e}") + save_progress(all_transaction_hashes, batch_num) + raise + + # Process new user setups (setupVestingPlans) + if new_setups: + print(f"\n{'=' * 50}") + print(f"Processing {len(new_setups)} new vesting plan setups...") + print( + f"Vesting period: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(VESTING_START_TIME))} to {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(VESTING_END_TIME))}" + ) + print(f"{'=' * 50}") + + total_setup_batches = (len(new_setups) + batch_size - 1) // batch_size + + for batch_num in range(total_setup_batches): + start_idx = batch_num * batch_size + end_idx = min((batch_num + 1) * batch_size, len(new_setups)) + + batch_data = new_setups[start_idx:end_idx] + batch_users = [user for user, _ in batch_data] + batch_amounts = [amount for _, amount in batch_data] + + print( + f"\nProcessing SETUP batch {batch_num + 1}/{total_setup_batches} ({len(batch_data)} users)..." + ) + + if dry_run: + batch_info = { + "operation": "setup", + "batch_number": batch_num + 1, + "users": batch_users, + "amounts": batch_amounts, + "user_updates": batch_data, + "start_time": VESTING_START_TIME, + "end_time": VESTING_END_TIME, + } + all_batch_data.append(batch_info) + else: + try: + tx_hash = send_setup_batch_transaction( + TOKEN_ADDRESS, batch_users, batch_amounts, batch_num + 1 + ) + all_transaction_hashes.append(("setup", tx_hash)) + time.sleep(2) + except Exception as e: + print(f"Error processing setup batch {batch_num + 1}: {e}") + save_progress(all_transaction_hashes, batch_num) + raise + + if dry_run: + save_dry_run_summary(all_batch_data, existing_updates, new_setups) + else: + save_progress(all_transaction_hashes, len(all_batch_data)) + print("\nAll batches processed successfully!") + print(f"Total transactions: {len(all_transaction_hashes)}") + print( + f"Reset transactions: {sum(1 for op, _ in all_transaction_hashes if op == 'reset')}" + ) + print( + f"Setup transactions: {sum(1 for op, _ in all_transaction_hashes if op == 'setup')}" + ) + + +def save_dry_run_summary( + all_batch_data: List[dict], + existing_updates: List[Tuple[str, int]], + new_setups: List[Tuple[str, int]], +): + """ + Save dry run summary to files for review + """ + timestamp = int(time.time()) + + # Save detailed batch information + batch_file = f"dry_run_batches_{timestamp}.json" + with open(batch_file, "w") as f: + json.dump(all_batch_data, f, indent=2) + + # Save summary CSV + summary_file = f"dry_run_summary_{timestamp}.csv" + with open(summary_file, "w", newline="") as f: + fieldnames = ["user", "operation", "amount", "batch_number"] + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + + for batch_info in all_batch_data: + batch_num = batch_info["batch_number"] + operation = batch_info["operation"] + for user, amount in batch_info["user_updates"]: + writer.writerow( + { + "user": user, + "operation": operation, + "amount": amount, + "batch_number": batch_num, + } + ) + + # Print summary statistics + total_users = len(existing_updates) + len(new_setups) + total_batches = len(all_batch_data) + + print("\n" + "=" * 50) + print("DRY RUN SUMMARY") + print("=" * 50) + print(f"Total users to process: {total_users}") + print(f" - Existing plans to update: {len(existing_updates)}") + print(f" - New plans to create: {len(new_setups)}") + print(f"Total batches: {total_batches}") + print(f"Token address: {TOKEN_ADDRESS}") + print(f"Vesting contract: {VESTING_CONTRACT_ADDRESS}") + + if new_setups: + print("\nNew vesting plans configuration:") + print( + f" - Start time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(VESTING_START_TIME))}" + ) + print( + f" - End time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(VESTING_END_TIME))}" + ) + + print(f"\nDetailed batch data saved to: {batch_file}") + print(f"Summary CSV saved to: {summary_file}") + print("\nReview these files before running with dry_run=False") + print("To execute these changes, run without --dry-run flag") + + +def save_progress(transaction_hashes: List[Tuple[str, str]], batches_processed: int): + """ + Save transaction hashes to a file for record keeping + """ + output_file = f"vesting_update_txs_{int(time.time())}.json" + + # Organize hashes by operation type + reset_txs = [tx for op, tx in transaction_hashes if op == "reset"] + setup_txs = [tx for op, tx in transaction_hashes if op == "setup"] + + with open(output_file, "w") as f: + json.dump( + { + "all_transaction_hashes": transaction_hashes, + "reset_transactions": reset_txs, + "setup_transactions": setup_txs, + "batches_processed": batches_processed, + "timestamp": int(time.time()), + }, + f, + indent=2, + ) + print(f"Progress saved to {output_file}") + + +def create_sample_input_file(): + """ + Create a sample CSV input file for testing + """ + import csv + + sample_data = [ + { + "user": "0x1234567890123456789012345678901234567890", + "amount": "1000000000000000000", + }, + { + "user": "0x2345678901234567890123456789012345678901", + "amount": "2000000000000000000", + }, + { + "user": "0x3456789012345678901234567890123456789012", + "amount": "1500000000000000000", + }, + # Add more users here + ] + + with open("vesting_updates_input.csv", "w", newline="") as f: + fieldnames = ["user", "amount"] + writer = csv.DictWriter(f, fieldnames=fieldnames) + + writer.writeheader() + writer.writerows(sample_data) + + print("Sample CSV input file created: vesting_updates_input.csv") + + +if __name__ == "__main__": + import sys + + # Check command line arguments + dry_run = False + if len(sys.argv) > 1 and sys.argv[1] == "--dry-run": + dry_run = True + + # Uncomment to create a sample input file + create_sample_input_file() + + # Configuration + input_file = "vesting_updates_input.csv" # Your CSV input file + batch_size = 300 + + # Check if input file exists + if not os.path.exists(input_file): + print(f"Error: Input file '{input_file}' not found!") + print("Please create a CSV file with 'user' and 'amount' columns.") + sys.exit(1) + + print("Starting vesting plan updates...") + print(f"Input file: {input_file}") + print(f"Batch size: {batch_size}") + print(f"Mode: {'DRY RUN' if dry_run else 'LIVE'}") + + if not dry_run: + response = input("\nThis will send REAL transactions. Continue? (yes/no): ") + if response.lower() != "yes": + print("Aborted.") + sys.exit(0) + + # Run the update + update_vesting_plans(input_file, batch_size=batch_size, dry_run=dry_run) diff --git a/scripts/py/vesting_upsert_manager.py b/scripts/py/vesting_upsert_manager.py new file mode 100644 index 0000000..8a44431 --- /dev/null +++ b/scripts/py/vesting_upsert_manager.py @@ -0,0 +1,74 @@ +import csv +import json + +from web3 import Web3 + +# --- Config --- +rpc_url = "https://1rpc.io/base" +PRIVATE_KEY = "" +upsert_manager_address = "" + +# --- Setup --- +w3 = Web3(Web3.HTTPProvider(rpc_url)) +account = w3.eth.account.from_key(PRIVATE_KEY) + +with open("vesting_upsert_manager.json") as f: + upsert_manager_abi = json.load(f) + +upsert_manager_contract = w3.eth.contract(address=w3.to_checksum_address(upsert_manager_address), + abi=upsert_manager_abi) + +# --- Call Data --- +token = w3.to_checksum_address("0x800822d361335b4d5F352Dac293cA4128b5B605f") +users = [] +amounts = [] +with open("input.csv", newline="") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + user = w3.to_checksum_address(row["user"].strip()) + amount_str = row["amount"].strip() + if not amount_str: + continue + amount = int(amount_str) + users.append(user) + amounts.append(amount) + +print(f"Loaded {len(users)} users from CSV") + +# --- Send tx --- +batch_size = 100 +for i in range(len(users) // batch_size + 1): + chunk_users = users[batch_size * i:batch_size * (i + 1)] + chunk_amounts = amounts[batch_size * i:batch_size * (i + 1)] + + if not chunk_users: + continue + + nonce = w3.eth.get_transaction_count(account.address) + + estimated_gas = upsert_manager_contract.functions.upsertVestingPlans( + token, + chunk_users, + chunk_amounts + ).estimate_gas({'from': account.address}) + + tx = upsert_manager_contract.functions.upsertVestingPlans( + token, + chunk_users, + chunk_amounts + ).build_transaction({ + 'chainId': w3.eth.chain_id, + 'from': account.address, + 'nonce': nonce, + 'gas': int(estimated_gas * 1.2), + 'gasPrice': int(w3.eth.gas_price * 1.2), + }) + + signed_tx = w3.eth.account.sign_transaction(tx, private_key=PRIVATE_KEY) + tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) + + print("Transaction sent:", w3.to_hex(tx_hash)) + receipt = w3.eth.wait_for_transaction_receipt(tx_hash) + print("Transaction confirmed.", receipt) + +print("Finished successfully") diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml new file mode 100644 index 0000000..9594778 --- /dev/null +++ b/scripts/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "scripts" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "eth-account>=0.13.7", + "multicallable>=7.2.1", + "web3>=7.12.0", +] diff --git a/tasks/index.ts b/tasks/index.ts new file mode 100644 index 0000000..2dc01e0 --- /dev/null +++ b/tasks/index.ts @@ -0,0 +1,9 @@ +import "./symmVestingPlanInitializer" +import "./symmioToken" +import "./symmStaking" +import "./symmAllocationClaimer" +import "./symmVesting" +import "./symmVestingPlanInitializerSetup" +import "./symmVestingV2" +import "./symmioBuildersNft" +import "./symmioBuildersNftManager" \ No newline at end of file diff --git a/tasks/symmStaking.ts b/tasks/symmStaking.ts index 5e1f834..9afa255 100644 --- a/tasks/symmStaking.ts +++ b/tasks/symmStaking.ts @@ -1,16 +1,87 @@ -import { task } from "hardhat/config"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { task, types } from "hardhat/config" -task("deploy:SymmStaking", "Deploys the SymmStaking contract") +task("deploy:SymmStaking", "Deploys the SymmStaking logic and proxy using CREATE2") .addParam("admin", "The admin of the SymmStaking contract") - .addParam("stakingToken", "The address of the staking token") - .setAction(async ({admin, stakingToken}, { ethers, upgrades }: HardhatRuntimeEnvironment) => { - console.log("deploy:SymmStaking"); + .addParam("token", "The address of the staking token") + .addParam("factory", "The deployed Create2Factory contract address") + .addParam("implsalt", "Salt for deploying the implementation contract", undefined, types.string, true) + .addParam("proxysalt", "Salt for deploying the proxy contract", undefined, types.string, true) + .setAction(async ({ admin, token, factory, implsalt, proxysalt }, { ethers }) => { + console.log("Deploying deterministic contracts for SymmStaking...") + const dryRun = false - const SymmStakingFactory = await ethers.getContractFactory("SymmStaking"); - const symmStakingContract = await upgrades.deployProxy(SymmStakingFactory, [admin, stakingToken], { initializer: "initialize" }); - await symmStakingContract.waitForDeployment(); + // 1. Get the contract factory for the logic contract + const SymmStakingFactory = await ethers.getContractFactory("SymmStaking") - console.log(`SymmStaking Contract deployed at: ${await symmStakingContract.getAddress()}`); - return symmStakingContract; - }); + // 2. Get an instance of your Create2Factory contract + const create2Factory = await ethers.getContractAt("Create2Factory", factory) + + // 3. Prepare implementation deployment bytecode + const implDeployTx = await SymmStakingFactory.getDeployTransaction() + const implBytecode = implDeployTx.data + if (!implBytecode) { + throw new Error("Cannot obtain implementation deployment bytecode") + } + + // 4. Compute a deterministic salt for implementation if not provided + const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`staking`)) + console.log("Implementation salt:", implementationSalt) + + // 5. Compute the predicted implementation address + // const predictedImplAddress = await create2Factory.getAddress(implBytecode, implementationSalt) + // const predictedImplAddress = (await create2Factory.functions.getAddress(implBytecode, implementationSalt))[0]; + const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt) + console.log("Predicted implementation address:", predictedImplAddress) + + if (!dryRun) { + // 6. Deploy the implementation via the factory using CREATE2 + console.log("Deploying implementation via CREATE2...") + const implTx = await create2Factory.deploy(implBytecode, implementationSalt) + await implTx.wait() + console.log("Implementation deployed at:", predictedImplAddress) + } + + console.log() + + // 7. Encode initializer data: initialize(admin, stakingToken) + const initData = SymmStakingFactory.interface.encodeFunctionData("initialize", [admin, token]) + + console.log("Deploying TransparentUpgradeableProxy with following params") + console.log(predictedImplAddress, admin, initData) + + // 8. Get the TransparentUpgradeableProxy factory + const TransparentUpgradeableProxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy") + + // 9. Prepare proxy deployment bytecode + // TransparentUpgradeableProxy constructor parameters: (logic, admin, data) + const proxyDeployTx = await TransparentUpgradeableProxyFactory.getDeployTransaction(predictedImplAddress, admin, initData) + const proxyBytecode = proxyDeployTx.data + if (!proxyBytecode) { + throw new Error("Cannot obtain proxy deployment bytecode") + } + + // 10. Compute a deterministic salt for proxy if not provided + const proxySaltValue = proxysalt || ethers.keccak256(ethers.toUtf8Bytes(`proxy-staking`)) + console.log("Proxy salt:", proxySaltValue) + + // console.log(proxyBytecode) + + // 11. Compute the predicted proxy address + const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue) + console.log("Predicted proxy address:", predictedProxyAddress) + + if (!dryRun) { + // 12. Deploy the proxy via the factory using CREATE2 + console.log("Deploying proxy via CREATE2...") + const proxyTx = await create2Factory.deploy(proxyBytecode, proxySaltValue) + await proxyTx.wait() + console.log("CREATE2 deployment confirmed.") + console.log("Deterministic TransparentUpgradeableProxy deployed at:", predictedProxyAddress) + } + + // return { + // implementation: predictedImplAddress, + // proxy: predictedProxyAddress, + // } + return await ethers.getContractAt("SymmStaking", predictedProxyAddress) + }) diff --git a/tasks/symmVesting.ts b/tasks/symmVesting.ts index 4d6b705..a6f0c47 100644 --- a/tasks/symmVesting.ts +++ b/tasks/symmVesting.ts @@ -1,34 +1,109 @@ -import { task } from "hardhat/config" -import { HardhatRuntimeEnvironment } from "hardhat/types" +import { task, types } from "hardhat/config"; -task("deploy:Vesting", "Deploys the Vesting contract") - .addParam("admin", "The admin of the Vesting contract") - .addParam("lockedClaimPenaltyReceiver", "Address that receives the penalty") +task("deploy:vesting", "Deploys the SymmVesting logic and proxy using CREATE2") + .addParam("admin", "The admin of the SymmVesting contract") + .addParam("penaltyreceiver", "Address that receives the penalty") .addParam("pool", "Address of the pool") .addParam("router", "Address of the router") .addParam("permit2", "Address of the permit2") .addParam("vault", "Address of the vault") .addParam("symm", "Address of symm token") .addParam("usdc", "Address of usdc token") - .addParam("symmLp", "Address of symmLp token") - .setAction(async ({ admin, lockedClaimPenaltyReceiver, pool, router, permit2, vault, symm, usdc, symmLp }, { ethers, upgrades }: HardhatRuntimeEnvironment) => { - console.log("deploy:Vesting") - - const VestingPlanOps = await ethers.getContractFactory("VestingPlanOps") - const vestingPlanOps = await VestingPlanOps.deploy() - await vestingPlanOps.waitForDeployment() - - const VestingFactory = await ethers.getContractFactory("SymmVesting", { - libraries: { - VestingPlanOps: await vestingPlanOps.getAddress(), - }, - }) - const vestingContract = await upgrades.deployProxy(VestingFactory, [admin, lockedClaimPenaltyReceiver, pool, router, permit2, vault, symm, usdc, symmLp ], { - unsafeAllow: ["external-library-linking"], - initializer: "initialize", - }) - await vestingContract.waitForDeployment() - - console.log(`SymmVesting Contract deployed at: ${await vestingContract.getAddress()}`) - return vestingContract - }) + .addParam("lp", "Address of lp token") + .addParam("factory", "The deployed Create2Factory contract address") + .addParam("implsalt", "Salt for deploying the implementation contract", undefined, types.string, true) + .addParam("proxysalt", "Salt for deploying the proxy contract", undefined, types.string, true) + .setAction(async ({ + admin, + penaltyreceiver, + pool, + router, + permit2, + vault, + symm, + usdc, + lp, + factory, + implsalt, + proxysalt, + }, { ethers }) => { + console.log("Deploying deterministic contracts for SymmVesting..."); + const dryRun = false; + + // - Get an instance of your Create2Factory contract + const create2Factory = await ethers.getContractAt("Create2Factory", factory); + + // - Get the contract factory for the logic contract + const SymmVestingFactory = await ethers.getContractFactory("SymmVesting", {}); + + // - Prepare implementation deployment bytecode + const implDeployTx = await SymmVestingFactory.getDeployTransaction(); + const implBytecode = implDeployTx.data; + if (!implBytecode) { + throw new Error("Cannot obtain implementation deployment bytecode"); + } + + // - Compute a deterministic salt for implementation if not provided + const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vesting`)); + console.log("Implementation salt:", implementationSalt); + + // - Compute the predicted implementation address + const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt); + console.log("Predicted implementation address:", predictedImplAddress); + + if (!dryRun) { + // - Deploy the implementation via the factory using CREATE2 + console.log("Deploying implementation via CREATE2..."); + const implTx = await create2Factory.deploy(implBytecode, implementationSalt); + await implTx.wait(); + console.log("Implementation deployed at:", predictedImplAddress); + console.log(); + } + + // - Encode initializer data + const initData = SymmVestingFactory.interface.encodeFunctionData("initialize", [ + admin, + penaltyreceiver, + pool, + router, + permit2, + vault, + symm, + usdc, + lp, + ]); + console.log("Deploying TransparentUpgradeableProxy with following params"); + console.log(predictedImplAddress, admin, initData); + + // - Get the TransparentUpgradeableProxy factory + const TransparentUpgradeableProxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy"); + + // 14. Prepare proxy deployment bytecode + // TransparentUpgradeableProxy constructor parameters: (logic, admin, data) + const proxyDeployTx = await TransparentUpgradeableProxyFactory.getDeployTransaction(predictedImplAddress, admin, initData); + const proxyBytecode = proxyDeployTx.data; + if (!proxyBytecode) { + throw new Error("Cannot obtain proxy deployment bytecode"); + } + + // - Compute a deterministic salt for proxy if not provided + const proxySaltValue = proxysalt || ethers.keccak256(ethers.toUtf8Bytes(`proxy-vesting`)); + console.log("Proxy salt:", proxySaltValue); + + // console.log(proxyBytecode) + + // - Compute the predicted proxy address + const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue); + console.log("Predicted proxy address:", predictedProxyAddress); + + if (!dryRun) { + // - Deploy the proxy via the factory using CREATE2 + console.log("Deploying proxy via CREATE2..."); + const proxyTx = await create2Factory.deploy(proxyBytecode, proxySaltValue); + await proxyTx.wait(); + console.log("CREATE2 deployment confirmed."); + console.log("Deterministic TransparentUpgradeableProxy deployed at:", predictedProxyAddress); + } + + return await ethers.getContractAt("SymmVesting", predictedProxyAddress); + }); diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts new file mode 100644 index 0000000..8db24d6 --- /dev/null +++ b/tasks/symmVestingPlanInitializer.ts @@ -0,0 +1,52 @@ +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { ethers } from "hardhat"; +import * as fs from "fs"; + +task("deploy:SymmVestingPlanInitializer", "Deploys the SymmVestingPlanInitializer contract") + .addParam("symmTokenAddress", "Address of the symm token") + .addParam("symmVestingAddress", "Address of the symmVestingContract") + .addParam("totalInitiatableSYMM", "Total initiatable symm") + .addParam("launchTimeStamp", "The of the launch in seconds") + .setAction(async ({ symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp }, { + ethers, + upgrades, + }: HardhatRuntimeEnvironment) => { + console.log("deploy:SymmVestingPlanInitializer"); + + const signers = await ethers.getSigners(); + const admin = signers[0]; + + const SymmVestingPlanInitializer = await ethers.getContractFactory("SymmVestingPlanInitializer"); + const symmVestingPlanInitializer = await SymmVestingPlanInitializer.deploy(admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp); + await symmVestingPlanInitializer.waitForDeployment(); + + const data = fs.readFileSync("user_available_symm.json", "utf8"); + const user_available: { + Users: string[], + Available: string[] + } = JSON.parse(data); + + const chunkSize = 100; + const users = user_available.Users; + const amounts = user_available.Available; + + fs.writeFileSync("error.log", "", { encoding: "utf-8" }); + for (let i = 0; i < users.length; i += chunkSize) { + const usersChunk = users.slice(i, i + chunkSize); + const amountsChunk = amounts.slice(i, i + chunkSize); + try { + await symmVestingPlanInitializer.connect(admin).setPendingAmounts(usersChunk, amountsChunk); + console.log(`${i}..${i + chunkSize}: OK`); + } catch (error) { + console.error(`Error in users=${usersChunk}, amounts=${amountsChunk}`, error); + const logMsg = `[Error in users=${JSON.stringify(usersChunk)}, amounts=${JSON.stringify(amountsChunk)}\n${(error as any).stack || error}\n\n`; + + fs.appendFileSync("error.log", logMsg, { encoding: "utf-8" }); + } + } + + console.log(`symmVestingPlanInitializer Contract deployed at: ${await symmVestingPlanInitializer.getAddress()}`); + return symmVestingPlanInitializer; + }, + ); diff --git a/tasks/symmVestingPlanInitializerSetup.ts b/tasks/symmVestingPlanInitializerSetup.ts new file mode 100644 index 0000000..aaf5d0b --- /dev/null +++ b/tasks/symmVestingPlanInitializerSetup.ts @@ -0,0 +1,47 @@ +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { ethers } from "hardhat"; +import * as fs from "fs"; + +task("SymmVestingPlanInitializerSetup", "Setup the SymmVestingPlanInitializer contract") + .addParam("deployedAddress", "Address initializer") + .setAction(async ({ deployedAddress }, { + ethers, + upgrades, + }: HardhatRuntimeEnvironment) => { + console.log("Setup SymmVestingPlanInitializer"); + + const signers = await ethers.getSigners(); + const admin = signers[0]; + + const symmVestingPlanInitializer = await ethers.getContractAt("SymmVestingPlanInitializer", deployedAddress) + + const data = fs.readFileSync("user_available_symm.json", "utf8"); + const user_available: { + Users: string[], + Available: string[] + } = JSON.parse(data); + + const chunkSize = 1000; + const users = user_available.Users; + const amounts = user_available.Available; + + fs.writeFileSync("error.log", "", { encoding: "utf-8" }); + for (let i = 0; i < users.length; i += chunkSize) { + const usersChunk = users.slice(i, i + chunkSize); + const amountsChunk = amounts.slice(i, i + chunkSize); + try { + await symmVestingPlanInitializer.connect(admin).setInitiatableVestingAmount(usersChunk, amountsChunk); + console.log(`${i}..${i + chunkSize}: OK`); + } catch (error) { + console.error(`Error in users=${usersChunk}, amounts=${amountsChunk}`, error); + const logMsg = `[Error in users=${JSON.stringify(usersChunk)}, amounts=${JSON.stringify(amountsChunk)}\n${(error as any).stack || error}\n\n`; + + fs.appendFileSync("error.log", logMsg, { encoding: "utf-8" }); + } + } + + console.log(`Set successfully!`); + return symmVestingPlanInitializer; + }, + ); diff --git a/tasks/symmVestingV2.ts b/tasks/symmVestingV2.ts new file mode 100644 index 0000000..eb0d151 --- /dev/null +++ b/tasks/symmVestingV2.ts @@ -0,0 +1,109 @@ +import { task, types } from "hardhat/config"; + +task("deploy:vestingV2", "Deploys the SymmVestingV2 logic and proxy using CREATE2") + .addParam("admin", "The admin of the SymmVestingV2 contract") + .addParam("penaltyreceiver", "Address that receives the penalty") + .addParam("pool", "Address of the pool") + .addParam("router", "Address of the router") + .addParam("permit2", "Address of the permit2") + .addParam("vault", "Address of the vault") + .addParam("symm", "Address of symm token") + .addParam("usdc", "Address of usdc token") + .addParam("lp", "Address of lp token") + .addParam("factory", "The deployed Create2Factory contract address") + .addParam("implsalt", "Salt for deploying the implementation contract", undefined, types.string, true) + .addParam("proxysalt", "Salt for deploying the proxy contract", undefined, types.string, true) + .setAction(async ({ + admin, + penaltyreceiver, + pool, + router, + permit2, + vault, + symm, + usdc, + lp, + factory, + implsalt, + proxysalt, + }, { ethers }) => { + console.log("Deploying deterministic contracts for SymmVestingV2..."); + const dryRun = false; + + // - Get an instance of your Create2Factory contract + const create2Factory = await ethers.getContractAt("Create2Factory", factory); + + // - Get the contract factory for the logic contract + const SymmVestingFactory = await ethers.getContractFactory("SymmVestingV2", {}); + + // - Prepare implementation deployment bytecode + const implDeployTx = await SymmVestingFactory.getDeployTransaction(); + const implBytecode = implDeployTx.data; + if (!implBytecode) { + throw new Error("Cannot obtain implementation deployment bytecode"); + } + + // - Compute a deterministic salt for implementation if not provided + const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vestingV2`)); + console.log("Implementation salt:", implementationSalt); + + // - Compute the predicted implementation address + const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt); + console.log("Predicted implementation address:", predictedImplAddress); + + if (!dryRun) { + // - Deploy the implementation via the factory using CREATE2 + console.log("Deploying implementation via CREATE2..."); + const implTx = await create2Factory.deploy(implBytecode, implementationSalt); + await implTx.wait(); + console.log("Implementation deployed at:", predictedImplAddress); + console.log(); + } + + // - Encode initializer data + const initData = SymmVestingFactory.interface.encodeFunctionData("initialize", [ + admin, + penaltyreceiver, + pool, + router, + permit2, + vault, + symm, + usdc, + lp, + ]); + console.log("Deploying TransparentUpgradeableProxy with following params"); + console.log(predictedImplAddress, admin, initData); + + // - Get the TransparentUpgradeableProxy factory + const TransparentUpgradeableProxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy"); + + // - Prepare proxy deployment bytecode + // TransparentUpgradeableProxy constructor parameters: (logic, admin, data) + const proxyDeployTx = await TransparentUpgradeableProxyFactory.getDeployTransaction(predictedImplAddress, admin, initData); + const proxyBytecode = proxyDeployTx.data; + if (!proxyBytecode) { + throw new Error("Cannot obtain proxy deployment bytecode"); + } + + // - Compute a deterministic salt for proxy if not provided + const proxySaltValue = proxysalt || ethers.keccak256(ethers.toUtf8Bytes(`proxy-vesting`)); + console.log("Proxy salt:", proxySaltValue); + + // console.log(proxyBytecode) + + // - Compute the predicted proxy address + const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue); + console.log("Predicted proxy address:", predictedProxyAddress); + + if (!dryRun) { + // - Deploy the proxy via the factory using CREATE2 + console.log("Deploying proxy via CREATE2..."); + const proxyTx = await create2Factory.deploy(proxyBytecode, proxySaltValue); + await proxyTx.wait(); + console.log("CREATE2 deployment confirmed."); + console.log("Deterministic TransparentUpgradeableProxy deployed at:", predictedProxyAddress); + } + + return await ethers.getContractAt("SymmVestingV2", predictedProxyAddress); + }); diff --git a/tasks/symmioBuildersNft.ts b/tasks/symmioBuildersNft.ts new file mode 100644 index 0000000..c04a375 --- /dev/null +++ b/tasks/symmioBuildersNft.ts @@ -0,0 +1,29 @@ +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { ethers, upgrades } from "hardhat"; + +task("deploy:SymmioBuildersNft", "Deploys the SymmioBuildersNft contract") + .setAction(async ({}, { + ethers, + upgrades, + }: HardhatRuntimeEnvironment) => { + console.log("deploy:SymmioBuildersNft"); + + const signers = await ethers.getSigners(); + const admin = signers[0]; + + const symmioBuildersNft = await ethers.getContractFactory("SymmioBuildersNft"); + + const contract = await upgrades.deployProxy(symmioBuildersNft, [admin.address], { initializer: "initialize" }); + await contract.waitForDeployment(); + + const implDeployTx = await symmioBuildersNft.getDeployTransaction(); + const implBytecode = implDeployTx.data; + if (!implBytecode) { + throw new Error("Cannot obtain implementation deployment bytecode"); + } + + console.log(`symmioBuildersNft Contract deployed at ${await contract.getAddress()}`); + return contract; + }, + ); diff --git a/tasks/symmioBuildersNftManager.ts b/tasks/symmioBuildersNftManager.ts new file mode 100644 index 0000000..376ec94 --- /dev/null +++ b/tasks/symmioBuildersNftManager.ts @@ -0,0 +1,49 @@ +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { ethers, upgrades } from "hardhat"; + +task("deploy:SymmioBuildersNftManager", "Deploys the SymmioBuildersNftManager contract") + .addParam("symm", "address of symmToken") + .addParam("nft", "address of symmioBuildersNft") + .setAction(async ({ symm, nft }, { + ethers, + upgrades, + }: HardhatRuntimeEnvironment) => { + console.log("deploy:SymmioBuildersNftManager"); + + const signers = await ethers.getSigners(); + + const admin = signers[0]; + const minLockAmount = BigInt(100000000000000000000); + const cliffDuration = 10; + const vestingDuration = 3600; + const lockedClaimPenalty = 20; + const lockedClaimPenaltyReceiver = "0xBcd4042DE499D14e55001CcbB24a551F3b954096"; + + const symmioBuildersNftManager = await ethers.getContractFactory("SymmioBuildersNftManager"); + + const contract = await upgrades.deployProxy( + symmioBuildersNftManager, + [ + symm, + nft, + admin.address, + minLockAmount, + cliffDuration, + vestingDuration, + lockedClaimPenalty, + lockedClaimPenaltyReceiver, + ], + { initializer: "initialize" }); + await contract.waitForDeployment(); + + const implDeployTx = await symmioBuildersNftManager.getDeployTransaction(); + const implBytecode = implDeployTx.data; + if (!implBytecode) { + throw new Error("Cannot obtain implementation deployment bytecode"); + } + + console.log(`symmioBuildersNftManager Contract deployed at ${await contract.getAddress()}`); + return contract; + }, + ); diff --git a/tests/Initialize.fixture.ts b/tests/Initialize.fixture.ts index 3a472a5..2897258 100644 --- a/tests/Initialize.fixture.ts +++ b/tests/Initialize.fixture.ts @@ -1,8 +1,14 @@ -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" -import { ethers, run } from "hardhat" -import { e } from "../utils" -import { SymmAllocationClaimer, Symmio, Vesting, SymmStaking } from "../typechain-types" -import * as Process from "process"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { ethers, run } from "hardhat"; +import { + SymmAllocationClaimer, + Symmio, + SymmioBuildersNft, + SymmioBuildersNftManager, + SymmStaking, + SymmVestingPlanInitializer, + VestingV2, +} from "../typechain-types"; export class RunContext { signers!: { @@ -13,16 +19,19 @@ export class RunContext { user3: SignerWithAddress symmioFoundation: SignerWithAddress vestingPenaltyReceiver: SignerWithAddress - } - symmioToken!: Symmio - claimSymm!: SymmAllocationClaimer - vesting!: Vesting - symmStaking!: SymmStaking + }; + symmioToken!: Symmio; + claimSymm!: SymmAllocationClaimer; + vesting!: VestingV2; + symmStaking!: SymmStaking; + symmVestingVlanInitializer!: SymmVestingPlanInitializer; + symmioBuildersNft!: SymmioBuildersNft; + symmioBuildersNftManager!: SymmioBuildersNftManager; } export async function initializeFixture(): Promise { - let context = new RunContext() - const signers: SignerWithAddress[] = await ethers.getSigners() + let context = new RunContext(); + const signers: SignerWithAddress[] = await ethers.getSigners(); context.signers = { admin: signers[0], setter: signers[1], @@ -31,13 +40,13 @@ export async function initializeFixture(): Promise { user3: signers[4], symmioFoundation: signers[4], vestingPenaltyReceiver: signers[5], - } + }; context.symmioToken = await run("deploy:SymmioToken", { name: "SYMMIO", symbol: "SYMM", admin: await context.signers.admin.getAddress(), - }) + }); context.claimSymm = await run("deploy:SymmAllocationClaimer", { admin: await context.signers.admin.getAddress(), @@ -47,24 +56,66 @@ export async function initializeFixture(): Promise { mintFactor: "500000000000000000", //5e17 => %50 }) - context.vesting = await run("deploy:Vesting", { - admin: await context.signers.admin.getAddress(), - lockedClaimPenaltyReceiver: await context.signers.vestingPenaltyReceiver.getAddress(), - pool: Process.env.POOL, - router: Process.env.ROUTER, - permit2: Process.env.PERMIT2, - vault: Process.env.VAULT, - symm: Process.env.SYMM, - usdc: Process.env.USDC, - symmLp: Process.env.SYMM_LP - }) + // context.vesting = await run("deploy:vesting", { + // admin: await context.signers.admin.getAddress(), + // penaltyreceiver: await context.signers.vestingPenaltyReceiver.getAddress(), + // pool: Process.env.POOL, + // router: Process.env.ROUTER, + // permit2: Process.env.PERMIT2, + // vault: Process.env.VAULT, + // symm: Process.env.SYMM, + // usdc: Process.env.USDC, + // lp: Process.env.SYMM_LP, + // factory: Process.env.FACTORY, + // implsalt: "1", + // proxysalt: "2", + // }) - context.symmStaking = await run("deploy:SymmStaking", { - admin: await context.signers.admin.getAddress(), - stakingToken: await context.symmioToken.getAddress(), - }) + // context.vesting = await run("deploy:vestingV2", { + // admin: await context.signers.admin.getAddress(), + // penaltyreceiver: await context.signers.vestingPenaltyReceiver.getAddress(), + // pool: Process.env.POOL, + // router: Process.env.ROUTER, + // permit2: Process.env.PERMIT2, + // vault: Process.env.VAULT, + // symm: Process.env.SYMM, + // usdc: Process.env.USDC, + // lp: Process.env.SYMM_LP, + // factory: Process.env.FACTORY, + // implsalt: "1", + // proxysalt: "2", + // }) + + // context.symmStaking = await run("deploy:SymmStaking", { + // admin: await context.signers.admin.getAddress(), + // token: await context.symmioToken.getAddress(), + // factory: Process.env.FACTORY, + // }) + + // context.symmVestingVlanInitializer = await run("deploy:SymmVestingPlanInitializer", { + // symmTokenAddress: await context.symmioToken.getAddress(), + // symmVestingAddress: await context.vesting.getAddress(), + // totalInitiatableSYMM: "1000000000000000000000000000", //10Me18 + // launchTimeStamp: String(floor(Date.now() / 1000) + 7 * 24 * 60 * 60), + // }) + + context.symmioBuildersNft = await run("deploy:SymmioBuildersNft", {}); + + context.symmioBuildersNftManager = await run("deploy:SymmioBuildersNftManager", { + symm: await context.symmioToken.getAddress(), + nft: await context.symmioBuildersNft.getAddress(), + }); + + await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), context.signers.admin); + + await context.symmioBuildersNft.connect(context.signers.admin).grantRole(await context.symmioBuildersNft.MINTER_ROLE(), context.signers.admin); + await context.symmioBuildersNft.connect(context.signers.admin).grantRole(await context.symmioBuildersNft.BURNER_ROLE(), context.signers.admin); + + await context.symmioBuildersNftManager.connect(context.signers.admin).grantRole(await context.symmioBuildersNftManager.MINTER_ROLE(), context.signers.admin); + await context.symmioBuildersNftManager.connect(context.signers.admin).grantRole(await context.symmioBuildersNftManager.SETTER_ROLE(), context.signers.admin); - await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), context.signers.admin) + await context.symmioBuildersNft.connect(context.signers.admin).grantRole(await context.symmioBuildersNft.MINTER_ROLE(), await context.symmioBuildersNftManager.getAddress()); + await context.symmioBuildersNft.connect(context.signers.admin).grantRole(await context.symmioBuildersNft.BURNER_ROLE(), await context.symmioBuildersNftManager.getAddress()); const roles = [ await context.claimSymm.SETTER_ROLE(), @@ -74,9 +125,10 @@ export async function initializeFixture(): Promise { ] for (const role of roles) await context.claimSymm.grantRole(role, await context.signers.admin.getAddress()) - await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), await context.claimSymm.getAddress()) - await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), await context.signers.admin.getAddress()) - await context.symmStaking.grantRole(await context.symmStaking.REWARD_MANAGER_ROLE(), await context.signers.admin.getAddress()) + await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), await context.claimSymm.getAddress()); + await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), await context.signers.admin.getAddress()); + // await context.symmStaking.grantRole(await context.symmStaking.REWARD_MANAGER_ROLE(), await context.signers.admin.getAddress()) + // await context.vesting.grantRole(await context.vesting.SETTER_ROLE(), await context.symmVestingVlanInitializer.getAddress()) - return context + return context; } diff --git a/tests/main.ts b/tests/main.ts index a1bad9d..d82b316 100644 --- a/tests/main.ts +++ b/tests/main.ts @@ -3,36 +3,56 @@ import { shouldBehaveLikeSymmioToken } from "./symmioToken.behavior" import { shouldBehaveLikeSymmStaking } from "./symmStaking.behavior" import { shouldBehaveLikeSymmVesting } from "./symmVesting.behavior" import { ShouldBehaveLikeVesting } from "./vesting.behavior" +import { shouldBehaveLikeSymmVestingPlanInitializer} from "./symmVestingPlanInitializer.behavior" +import { ShouldBehaveLikeVestingV2 } from "./vestingV2.behavior"; +import { shouldBehaveLikeSymmioBuildersNft } from "./symmioBuildersNft.behavior"; +import { shouldBehaveLikeSymmioBuildersNftManager } from "./symmioBuildersNftManager.behavior"; describe("Symmio Token", () => { // if (process.env.TEST_MODE === "static") { describe("Static Tests", async function () { - describe("Symm token", async function () { - shouldBehaveLikeSymmioToken() - }) + // describe("Symm token", async function () { + // shouldBehaveLikeSymmioToken() + // }) - describe("Allocation Claimer", async function () { - shouldBehaveLikeSymmAllocationClaimer() - }) + // describe("Allocation Claimer", async function () { + // shouldBehaveLikeSymmAllocationClaimer() + // }) // describe("Airdrop Helper", async function () { // shouldBehaveLikeAirdropHelper() // Not adapted // }) - describe("Symm Staking", async function () { - shouldBehaveLikeSymmStaking() - }) + // describe("Symm Staking", async function () { + // shouldBehaveLikeSymmStaking() + // }) + + // describe("Vesting", async function () { + // ShouldBehaveLikeVesting() + // }) - describe("Vesting", async function () { - ShouldBehaveLikeVesting() + // describe("Symm Vesting Plan Initializer", async function () { + // shouldBehaveLikeSymmVestingPlanInitializer() + // }) + + // describe("Vesting V2", async function () { + // ShouldBehaveLikeVestingV2() + // }) + + // describe("Symmio Builders Nft", async function () { + // shouldBehaveLikeSymmioBuildersNft() + // }) + + describe("Symmio Builders Nft Manager", async function () { + shouldBehaveLikeSymmioBuildersNftManager() }) }) // } else if (process.env.TEST_MODE === "dynamic") { // Dynamic tests - describe("Dynamic Tests", async function () { - describe("Symm Vesting", async function () { - shouldBehaveLikeSymmVesting() - }) - }) + // describe("Dynamic Tests", async function () { + // describe("Symm Vesting", async function () { + // shouldBehaveLikeSymmVesting() + // }) + // }) // } }) diff --git a/tests/symmVestingPlanInitializer.behavior.ts b/tests/symmVestingPlanInitializer.behavior.ts new file mode 100644 index 0000000..b293538 --- /dev/null +++ b/tests/symmVestingPlanInitializer.behavior.ts @@ -0,0 +1,212 @@ +/* eslint-disable node/no-missing-import */ +import { expect } from "chai" +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers" +import { SymmVestingPlanInitializer, Vesting } from "../typechain-types" +import { initializeFixture, RunContext } from "./Initialize.fixture" +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" +import { ethers } from "hardhat" +import { NumberLike } from "@nomicfoundation/hardhat-network-helpers/dist/src/types" +import { BigNumberish } from "ethers" +import { e } from "../utils" + +export function shouldBehaveLikeSymmVestingPlanInitializer() { + let context: RunContext + let vestingPlanInitializer: SymmVestingPlanInitializer // keep the same naming pattern the user showed + let admin: SignerWithAddress + let user1: SignerWithAddress + let user2: SignerWithAddress + let launchTime: NumberLike + + beforeEach(async () => { + context = await loadFixture(initializeFixture) + vestingPlanInitializer = context.symmVestingVlanInitializer + ;({ admin, user1, user2 } = context.signers) + launchTime = await vestingPlanInitializer.launchDay() + }) + + /* ---------------------------------------------------------------------- */ + /* setPendingAmounts() tests */ + /* ---------------------------------------------------------------------- */ + describe("setPendingAmounts", () => { + it("should revert on mismatched array lengths", async () => { + await expect(vestingPlanInitializer.connect(admin).setPendingAmounts([user1.address], [100, 200])).to.be.revertedWithCustomError( + vestingPlanInitializer, + "MismatchedArrays", + ) + }) + + it("should reject callers without SETTER_ROLE", async () => { + await expect(vestingPlanInitializer.connect(user1).setPendingAmounts([user1.address], [1000])).to.be.reverted + }) + + it("should register user allocations correctly", async () => { + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1.address, user2.address], [1000, 2000]) + + expect(await vestingPlanInitializer.pendingAmount(user1.address)).to.equal(1000) + expect(await vestingPlanInitializer.pendingAmount(user2.address)).to.equal(2000) + expect(await vestingPlanInitializer.pendingTotal()).to.equal(3000) + + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1.address, user2.address], [150, 430]) + + expect(await vestingPlanInitializer.pendingAmount(user1.address)).to.equal(150) + expect(await vestingPlanInitializer.pendingAmount(user2.address)).to.equal(430) + expect(await vestingPlanInitializer.pendingTotal()).to.equal(430 + 150) + }) + + it("should enforce the global SYMM cap", async () => { + const overCap = ethers.parseEther("10000001") + await expect(vestingPlanInitializer.connect(admin).setPendingAmounts([user1.address], [overCap])).to.be.revertedWithCustomError( + vestingPlanInitializer, + "exceededMaxSymmAmount", + ) + }) + }) + + /* ---------------------------------------------------------------------- */ + /* startVesting() tests */ + /* ---------------------------------------------------------------------- */ + describe("startVesting", () => { + beforeEach(async () => { + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) + }) + + it("should revert if the caller has no initiatable amount", async () => { + await expect(vestingPlanInitializer.connect(user2).startVesting()).to.be.revertedWithCustomError(vestingPlanInitializer, "ZeroAmount") + }) + + it("should revert while the contract is paused", async () => { + await vestingPlanInitializer.connect(admin).pause() + await expect(vestingPlanInitializer.connect(user1).startVesting()).to.be.revertedWithCustomError(vestingPlanInitializer, "EnforcedPause") + }) + + it("should revert if launch time is not reached", async () => { + await expect(vestingPlanInitializer.connect(user1).startVesting()).to.be.reverted + }) + + it("should allow user to initiate it's vesting plan when admin has allowed him", async () => { + await time.increaseTo(launchTime) + + await expect(vestingPlanInitializer.connect(user1).startVesting()).to.not.be.reverted + + expect(await vestingPlanInitializer.pendingAmount(user1.address)).to.equal(0) + expect(await vestingPlanInitializer.vestedAmount(user1.address)).to.equal(1000) + }) + }) + + /* ---------------------------------------------------------------------- */ + /* Pausing */ + /* ---------------------------------------------------------------------- */ + describe("pause/unpause", () => { + it("should allow pausing and prevent vesting requests while paused", async () => { + await time.increaseTo(launchTime) + + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) + await vestingPlanInitializer.connect(admin).pause() + + await expect(vestingPlanInitializer.connect(user1).startVesting()).to.be.revertedWithCustomError(vestingPlanInitializer, "EnforcedPause") + + await vestingPlanInitializer.connect(admin).unpause() + + await expect(vestingPlanInitializer.connect(user1).startVesting()).to.not.be.reverted + }) + + it("should not allow non-registered addresses to pause/unpause", async () => { + await expect(vestingPlanInitializer.connect(user1).pause()).to.be.revertedWithCustomError( + vestingPlanInitializer, + "AccessControlUnauthorizedAccount", + ) + await expect(vestingPlanInitializer.connect(user1).unpause()).to.be.revertedWithCustomError( + vestingPlanInitializer, + "AccessControlUnauthorizedAccount", + ) + }) + }) + + /* ---------------------------------------------------------------------- */ + /* endTime() */ + /* ---------------------------------------------------------------------- */ + describe("endTime()", () => { + it("should extend linearly with penalty as days pass", async () => { + await time.increaseTo(launchTime) + const before = await vestingPlanInitializer.endTime() + expect(before).to.equal(Number(launchTime) + 180 * 24 * 60 * 60) + const tenDays = 10 * 24 * 60 * 60 + await time.increaseTo(Number(launchTime) + tenDays) + const after = await vestingPlanInitializer.endTime() + expect((Number(after) - Number(before)) / 60 / 60 / 24).to.equal(2.5) //after - before = (launch + 10 + 172.5) - (launch + 180) = 2.5 + }) + + it("should give maximum decay after 180 days", async () => { + const oneDay = 24 * 60 * 60 + const oneHundredEightyDays = 180 * oneDay + const tenDays = 10 * oneDay + await time.increaseTo(Number(launchTime) + oneHundredEightyDays + tenDays) + const endTime = await vestingPlanInitializer.endTime() + expect(endTime).to.equal(Number(launchTime) + oneHundredEightyDays + tenDays + 45 * oneDay) + }) + }) + + /* ---------------------------------------------------------------------- */ + /* View variables */ + /* ---------------------------------------------------------------------- */ + describe("viewVariables", () => { + beforeEach(async () => { + await time.increaseTo(launchTime) + }) + + it("should calculate pendingTotal and vestedAmount correctly while admin decreases initiatable amount", async () => { + const beforeInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() + expect(beforeInitiatableAmountSum).to.equal(0) + + const vestedAmount = await vestingPlanInitializer.vestedAmount(user1) + expect(vestedAmount).to.equal(0) + + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) + + const firstInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() + expect(firstInitiatableAmountSum).to.equal(1000) + + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [250]) + + const secondInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() + expect(secondInitiatableAmountSum).to.equal(250) + + await vestingPlanInitializer.connect(user1).startVesting() + + expect(await vestingPlanInitializer.pendingAmount(user1)).to.equal(0) + expect(await vestingPlanInitializer.vestedAmount(user1)).to.equal(250) + }) + + it("should calculate pendingTotal and vestedAmount correctly while admin increases initiatable amount", async () => { + const beforeInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() + expect(beforeInitiatableAmountSum).to.equal(0) + + const vestedAmount = await vestingPlanInitializer.vestedAmount(user1) + expect(vestedAmount).to.equal(0) + + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) + + const firstInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() + expect(firstInitiatableAmountSum).to.equal(1000) + + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [2500]) + + const secondInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() + expect(secondInitiatableAmountSum).to.equal(2500) + + await vestingPlanInitializer.connect(user1).startVesting() + + expect(await vestingPlanInitializer.pendingAmount(user1)).to.equal(0) + expect(await vestingPlanInitializer.vestedAmount(user1)).to.equal(2500) + expect(secondInitiatableAmountSum).to.equal(2500) + }) + + it("should have 0.25e18 as penalty per day and 180 days for total days", async () => { + const penaltyPerDay = await vestingPlanInitializer.PENALTY_PER_DAY_BP() + expect(penaltyPerDay).to.equal(e("0.25")) + + const totalDays = await vestingPlanInitializer.VESTING_DURATION() + expect(totalDays).to.equal(60 * 24 * 60 * 60) // 60 days + }) + }) +} diff --git a/tests/symmioBuildersNft.behavior.ts b/tests/symmioBuildersNft.behavior.ts new file mode 100644 index 0000000..11b99db --- /dev/null +++ b/tests/symmioBuildersNft.behavior.ts @@ -0,0 +1,136 @@ +/* eslint-disable node/no-missing-import */ +import { expect } from "chai"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { SymmioBuildersNft } from "../typechain-types"; +import { initializeFixture, RunContext } from "./Initialize.fixture"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { ethers } from "hardhat"; +import { NumberLike } from "@nomicfoundation/hardhat-network-helpers/dist/src/types"; + +export function shouldBehaveLikeSymmioBuildersNft() { + let context: RunContext; + let symmioBuildersNft: SymmioBuildersNft; // keep the same naming pattern the user showed + let admin: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let launchTime: NumberLike; + + + beforeEach(async () => { + context = await loadFixture(initializeFixture); + symmioBuildersNft = context.symmioBuildersNft + ;({ admin, user1, user2 } = context.signers); + }); + + /* ---------------------------------------------------------------------- */ + /* setPendingAmounts() tests */ + /* ---------------------------------------------------------------------- */ + describe("SymmioBuildersNft - mint", () => { + it("should mint a token with correct lock data and emit event", async () => { + const to = await user1.getAddress(); + const amount = ethers.parseEther("10"); + const name = "Test NFT"; + + const tx = await symmioBuildersNft.connect(admin).mint(to, amount, name); + const receipt = await tx.wait(); + const logs = receipt!.logs; + + const iface = symmioBuildersNft.interface; + const parsedLog = logs + .map((log) => { + try { + return iface.parseLog(log); + } catch { + return null; + } + }) + .find((log) => log?.name === "NFTMinted"); + + expect(parsedLog).to.not.be.undefined; + const tokenId = parsedLog?.args.tokenId; + + + // Check token ownership + expect(await symmioBuildersNft.ownerOf(tokenId)).to.equal(to); + + // Check lockData + const lock = await symmioBuildersNft.lockData(tokenId); + expect(lock.amount).to.equal(amount); + expect(lock.name).to.equal(name); + expect(lock.unlockingAmount).to.equal(0); + expect(lock.lockTimestamp).to.be.gt(0); + }); + + it("should fail if caller does not have MINTER_ROLE", async () => { + const to = await user1.getAddress(); + const amount = ethers.parseEther("5"); + const name = "Unauthorized"; + + await expect(symmioBuildersNft.connect(user1).mint(to, amount, name)).to.be.rejectedWith(`AccessControl`); + }); + + }); + + describe("SymmioBuildersNFT - burn", () => { + it("should burn token and delete lockData", async () => { + const to = await user1.getAddress(); + const amount = ethers.parseEther("10"); + const name = "Test NFT"; + + const tx = await symmioBuildersNft.connect(admin).mint(to, amount, name); + await tx.wait(); + + const tokenId = 0; + + // Check lockData exists before burn + let lock = await symmioBuildersNft.lockData(tokenId); + expect(lock.amount).to.equal(amount); + + // Burn the token + await expect(symmioBuildersNft.connect(admin).burn(tokenId)).to.not.be.reverted; + + lock = await symmioBuildersNft.lockData(tokenId); + expect(lock.amount).to.equal(0); + expect(lock.name).to.equal(""); + expect(lock.unlockingAmount).to.equal(0); + expect(lock.lockTimestamp).to.be.equal(0); + }); + + it("should revert if caller does not have BURNER_ROLE", async () => { + const tokenId = 0; + await expect(symmioBuildersNft.connect(user1).burn(tokenId)).to.be.rejectedWith(`AccessControl`); + }); + }); + + describe("SymmioBuildersNft - updateLockData", function() { + it("should allow MINTER_ROLE to update lockData and emit event", async function() { + const to = await user1.getAddress(); + const amount = ethers.parseEther("10"); + const name = "Test NFT"; + + const tx = await symmioBuildersNft.connect(admin).mint(to, amount, name); + await tx.wait(); + + const tokenId = 0; + const newAmount = 2000; + const newUnlockingAmount = 500; + const newName = "Updated Name"; + + await expect(symmioBuildersNft.updateLockData(tokenId, newAmount, newUnlockingAmount, newName)) + .to.emit(symmioBuildersNft, "LockDataUpdated") + .withArgs(tokenId, newAmount, newUnlockingAmount, newName); + + const data = await symmioBuildersNft.lockData(tokenId); + expect(data.amount).to.equal(newAmount); + expect(data.unlockingAmount).to.equal(newUnlockingAmount); + expect(data.name).to.equal(newName); + }); + + it("should revert if caller does not have MINTER_ROLE", async function() { + const tokenId = 0; + await expect( + symmioBuildersNft.connect(user1).updateLockData(tokenId, 1, 2, "NoAccess"), + ).to.be.rejectedWith(`AccessControl`); + }); + }); +} diff --git a/tests/symmioBuildersNftManager.behavior.ts b/tests/symmioBuildersNftManager.behavior.ts new file mode 100644 index 0000000..6189eb9 --- /dev/null +++ b/tests/symmioBuildersNftManager.behavior.ts @@ -0,0 +1,664 @@ +/* eslint-disable node/no-missing-import */ +import { expect } from "chai"; +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers"; +import { Symmio, SymmioBuildersNft, SymmioBuildersNftManager } from "../typechain-types"; +import { initializeFixture, RunContext } from "./Initialize.fixture"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { ethers } from "hardhat"; +import { NumberLike } from "@nomicfoundation/hardhat-network-helpers/dist/src/types"; + +export function shouldBehaveLikeSymmioBuildersNftManager() { + let context: RunContext; + let symmioBuildersNftManager: SymmioBuildersNftManager; + let symmioBuildersNft: SymmioBuildersNft; + let symmioToken: Symmio; + let admin: SignerWithAddress; + let user1: SignerWithAddress; + let user2: SignerWithAddress; + let launchTime: NumberLike; + const mintAmount = ethers.parseEther("200"); + const minLockAmount = ethers.parseEther("100"); + const brand = "MyBrand"; + + + beforeEach(async () => { + context = await loadFixture(initializeFixture); + symmioBuildersNftManager = context.symmioBuildersNftManager; + symmioBuildersNft = context.symmioBuildersNft; + symmioToken = context.symmioToken; + ({ admin, user1, user2 } = context.signers); + + await symmioToken.connect(admin).mint(await user1.getAddress(), mintAmount); + await symmioToken.connect(user1).approve(await symmioBuildersNftManager.getAddress(), mintAmount); + }); + + /* ---------------------------------------------------------------------- */ + /* setPendingAmounts() tests */ + /* ---------------------------------------------------------------------- */ + describe("SymmioLocker - mintAndLock", () => { + it("should mint and lock successfully", async () => { + const tx = await symmioBuildersNftManager.connect(user1).mintAndLock(mintAmount, brand); + const receipt = await tx.wait(); + + const logs = receipt!.logs; + + const iface = symmioBuildersNftManager.interface; + const parsedLog = logs + .map((log) => { + try { + return iface.parseLog(log); + } catch { + return null; + } + }) + .find((log) => log?.name === "NFTMinted"); + + expect(parsedLog).to.not.be.undefined; + const tokenId = parsedLog?.args.tokenId; + + // Check token ownership + expect(await symmioBuildersNft.ownerOf(tokenId)).to.equal(await user1.getAddress()); + + // Check lockData + const lock = await symmioBuildersNft.lockData(tokenId); + expect(lock.amount).to.equal(mintAmount); + expect(lock.name).to.equal(brand); + expect(lock.unlockingAmount).to.equal(0); + expect(lock.lockTimestamp).to.be.gt(0); + }); + + it("should revert if amount < minLockAmount", async () => { + const lowAmount = ethers.parseEther("10"); + await expect(symmioBuildersNftManager.connect(user1).mintAndLock(lowAmount, brand)).to.be.revertedWithCustomError( + symmioBuildersNftManager, + "AmountBelowMinimum", + ).withArgs(lowAmount, minLockAmount); + }); + + it("should burn SYMM tokens from user", async () => { + const balanceBefore = await symmioToken.balanceOf(await user1.getAddress()); + await symmioBuildersNftManager.connect(user1).mintAndLock(mintAmount, brand); + const balanceAfter = await symmioToken.balanceOf(await user1.getAddress()); + expect(balanceAfter).to.equal(balanceBefore - mintAmount); + }); + }); + + describe("SymmioLocker - mintWithoutBurn", function() { + it("should mint NFT without burning and emit event", async () => { + await expect(symmioBuildersNftManager.mintWithoutBurn(await user1.getAddress(), mintAmount, brand)) + .to.emit(symmioBuildersNftManager, "NFTMintedWithoutBurn") + .withArgs(await admin.getAddress(), await user1.getAddress(), 0, mintAmount, brand); + + const lockData = await symmioBuildersNft.getLockData(0); + expect(lockData.amount).to.equal(mintAmount); + expect(lockData.name).to.equal(brand); + }); + + it("should revert if amount < minLockAmount", async () => { + const smallAmount = ethers.parseEther("5"); + + await expect( + symmioBuildersNftManager.mintWithoutBurn(await user1.getAddress(), smallAmount, brand), + ).to.be.revertedWithCustomError(symmioBuildersNftManager, "AmountBelowMinimum"); + }); + + it("should revert if recipient is zero address", async () => { + await expect( + symmioBuildersNftManager.mintWithoutBurn("0x0000000000000000000000000000000000000000", mintAmount, brand), + ).to.be.revertedWithCustomError(symmioBuildersNftManager, "ZeroAddress"); + }); + + it("should revert if caller does not have MINTER_ROLE", async () => { + await expect( + symmioBuildersNftManager.connect(user1).mintWithoutBurn(await user1.getAddress(), mintAmount, brand), + ).to.be.rejectedWith("AccessControl"); + }); + }); + + describe("SymmioLocker - lock", function() { + const lockAmount = ethers.parseEther("150"); + + it("should lock SYMM tokens and update lockData", async () => { + const tx = await symmioBuildersNft.connect(admin).mint(await user1.getAddress(), mintAmount, brand); + const receipt = await tx.wait(); + + const logs = receipt!.logs; + + const iface = symmioBuildersNftManager.interface; + const parsedLog = logs + .map((log) => { + try { + return iface.parseLog(log); + } catch { + return null; + } + }) + .find((log) => log?.name === "NFTMinted"); + + expect(parsedLog).to.not.be.undefined; + const tokenId = parsedLog?.args.tokenId; + + + const before = await symmioBuildersNft.getLockData(tokenId); + expect(before.amount).to.equal(mintAmount); + + await expect(symmioBuildersNftManager.connect(user1).lock(tokenId, lockAmount)) + .to.emit(symmioBuildersNftManager, "TokenLocked") + .withArgs(user1.address, tokenId, lockAmount); + + const after = await symmioBuildersNft.getLockData(tokenId); + expect(after.amount).to.equal(mintAmount + lockAmount); + }); + + it("should revert on zero amount", async () => { + await expect(symmioBuildersNftManager.connect(user1).lock(0, 0)).to.be.rejectedWith("ZeroAmount"); + }); + + it("should revert if not enough allowance", async () => { + await symmioToken.connect(user1).approve(await symmioBuildersNftManager.getAddress(), 0); + await expect(symmioBuildersNftManager.connect(user1).lock(0, lockAmount)).to.be.revertedWithCustomError( + symmioToken, + "ERC20InsufficientAllowance", + ); + }); + }); + + describe("SymmioLocker - merge()", function() { + it("should merge source into target and burn source", async () => { + const targetId = 0; + const sourceId = 1; + + const tx1 = await symmioBuildersNft.connect(admin).mint(await user1.getAddress(), mintAmount, brand); + await tx1.wait(); + const tx2 = await symmioBuildersNft.connect(admin).mint(await user1.getAddress(), mintAmount, brand); + await tx2.wait(); + + await expect(symmioBuildersNftManager.connect(user1).merge(targetId, sourceId)) + .to.emit(symmioBuildersNftManager, "TokensMerged") + .withArgs(targetId, sourceId, mintAmount + mintAmount); + + const targetData = await symmioBuildersNft.getLockData(targetId); + expect(targetData.amount).to.equal(mintAmount + mintAmount); + + await expect(symmioBuildersNft.ownerOf(sourceId)).to.be.reverted; // burned + }); + + it("should revert if caller does not own both tokens 1", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("10"), "Not yours"); + + await expect( + symmioBuildersNftManager.connect(user1).merge(0, 2), + ).to.be.revertedWithCustomError(symmioBuildersNftManager, "NotTokenOwner"); + }); + + it("should revert if caller does not own both tokens 2", async () => { + await symmioBuildersNft.mint(await user1.getAddress(), ethers.parseEther("10"), "Yours"); + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("11"), "Not yours"); + + await expect( + symmioBuildersNftManager.connect(user1).merge(0, 1), + ).to.be.revertedWithCustomError(symmioBuildersNftManager, "NotTokenOwner"); + }); + + it("should revert if merging same token", async () => { + const tx = await symmioBuildersNft.connect(admin).mint(await user1.getAddress(), mintAmount, brand); + await tx.wait(); + await expect( + symmioBuildersNftManager.connect(user1).merge(0, 0), + ).to.be.revertedWithCustomError(symmioBuildersNftManager, "InvalidMerge"); + }); + + it("should revert if source token has unlockingAmount > 0", async () => { + const tx1 = await symmioBuildersNft.connect(admin).mint(await user1.getAddress(), mintAmount, brand); + await tx1.wait(); + const tx2 = await symmioBuildersNft.connect(admin).mint(await user1.getAddress(), mintAmount, brand); + await tx2.wait(); + const newUnlockAmount = ethers.parseEther("100"); + + const tx = await symmioBuildersNftManager.connect(user1).initiateUnlock(1, newUnlockAmount); + await tx.wait(); + + await expect( + symmioBuildersNftManager.connect(user1).merge(0, 1), + ).to.be.revertedWithCustomError(symmioBuildersNftManager, "TokenHasActiveUnlock"); + }); + }); + + describe("SymmioLocker - initiateUnlock", function() { + it("should initiate unlock with correct state", async () => { + const unlockAmount = ethers.parseEther("50"); + + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Yours"); + + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, unlockAmount); + await tx.wait(); + + const unlockData = await symmioBuildersNftManager.unlockRequests(0); + + expect(unlockData.amount).to.equal(unlockAmount); + expect(unlockData.tokenId).to.equal(0); + expect(unlockData.owner).to.equal(admin.address); + expect(unlockData.vestingStarted).to.be.false; + }); + + it("should revert if not NFT owner", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("10"), "Not yours"); + await expect(symmioBuildersNftManager.connect(user1).initiateUnlock(0, ethers.parseEther("10"))) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "NotTokenOwner"); + }); + + it("should revert if amount is 0", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("10"), "Yours"); + await expect(symmioBuildersNftManager.connect(admin).initiateUnlock(0, 0)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "ZeroAmount"); + }); + + it("should revert if amount exceeds available locked amount", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("10"), "Yours"); + + const lockAmount = ethers.parseEther("100"); + + await expect(symmioBuildersNftManager.connect(admin).initiateUnlock(0, lockAmount + 1n)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "InsufficientLockedAmount"); + }); + }); + + describe("SymmioLocker - cancelUnlock", function() { + it("should cancel unlock and update lockData correctly", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Yours"); + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, ethers.parseEther("50")); + await tx.wait(); + + const requestBefore = await symmioBuildersNftManager.unlockRequests(0); + const dataBefore = await symmioBuildersNft.getLockData(0); + + await expect(symmioBuildersNftManager.cancelUnlock(0)) + .to.emit(symmioBuildersNftManager, "UnlockCancelled") + .withArgs(0, 0, admin.address, requestBefore.amount); + + const requestAfter = await symmioBuildersNftManager.unlockRequests(0); + expect(requestAfter.amount).to.equal(0); + + const dataAfter = await symmioBuildersNft.getLockData(0); + expect(dataAfter.unlockingAmount).to.equal(dataBefore.unlockingAmount - requestBefore.amount); + + const tokenUnlockIds = await symmioBuildersNftManager.getTokenUnlockIds(0); + expect(tokenUnlockIds).to.not.include(0); + }); + + it("should revert if caller is not unlock owner", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Not yours"); + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, ethers.parseEther("50")); + await tx.wait(); + + await expect(symmioBuildersNftManager.connect(user1).cancelUnlock(0)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "NotTokenOwner"); + }); + + it("should revert if unlock ID doesn't exist", async () => { + await expect(symmioBuildersNftManager.cancelUnlock(9999)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "UnlockNotFound"); + }); + + it("should revert if vesting already started", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Yours"); + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, ethers.parseEther("50")); + await tx.wait(); + + const cliff = await symmioBuildersNftManager.cliffDuration(); + await time.increase(Number(cliff) + 1); + + await symmioBuildersNftManager.completeCliffAndStartVesting(0); // assumes helper in test or mock + await expect(symmioBuildersNftManager.cancelUnlock(0)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "VestingAlreadyStarted"); + }); + }); + + describe("SymmioLocker - completeCliffAndStartVesting", function() { + it("should complete cliff and start vesting correctly", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Yours"); + const tx1 = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, ethers.parseEther("50")); + await tx1.wait(); + + // Increase time to pass cliff + const cliff = await symmioBuildersNftManager.cliffDuration(); + await time.increase(Number(cliff) + 1); + + const dataBefore = await symmioBuildersNft.getLockData(0); + + const tx2 = await symmioBuildersNftManager.completeCliffAndStartVesting(0); + await tx2.wait(); + + const request = await symmioBuildersNftManager.unlockRequests(0); + expect(request.vestingStarted).to.be.true; + expect(request.vestingPlanId).to.equal(0); + + const dataAfter = await symmioBuildersNft.getLockData(0); + expect(dataAfter.unlockingAmount).to.equal(dataBefore.unlockingAmount - request.amount); + }); + + it("should revert if cliff not passed", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Yours"); + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, ethers.parseEther("50")); + await tx.wait(); + + await expect(symmioBuildersNftManager.completeCliffAndStartVesting(0)).to.be.revertedWithCustomError( + symmioBuildersNftManager, + "CliffNotPassed", + ); + }); + + it("should revert if unlockId is invalid", async () => { + await expect(symmioBuildersNftManager.completeCliffAndStartVesting(999)).to.be.revertedWithCustomError( + symmioBuildersNftManager, + "UnlockNotFound", + ); + }); + + it("should revert if not unlock owner", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Yours"); + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, ethers.parseEther("50")); + await tx.wait(); + + const cliff = await symmioBuildersNftManager.cliffDuration(); + await time.increase(Number(cliff) + 1); + + await expect(symmioBuildersNftManager.connect(user1).completeCliffAndStartVesting(0)).to.be.revertedWithCustomError( + symmioBuildersNftManager, + "NotTokenOwner", + ); + }); + + it("should revert if vesting already started", async () => { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("80"), "Yours"); + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(0, ethers.parseEther("50")); + await tx.wait(); + + const cliff = await symmioBuildersNftManager.cliffDuration(); + await time.increase(Number(cliff) + 1); + await symmioBuildersNftManager.completeCliffAndStartVesting(0); + + await expect(symmioBuildersNftManager.completeCliffAndStartVesting(0)).to.be.revertedWithCustomError( + symmioBuildersNftManager, + "VestingAlreadyStarted", + ); + }); + + it("should burn NFT if all amounts = 0", async () => { + const unlockAmount = ethers.parseEther("100"); + + // Mint and lock + const tx1 = await symmioBuildersNftManager.connect(user1).mintAndLock(unlockAmount, "FullLock"); + await tx1.wait(); + + const tokenId = 0; + + // Initiate full unlock + const tx2 = await symmioBuildersNftManager.connect(user1).initiateUnlock(tokenId, unlockAmount); + await tx2.wait(); + + // Simulate that amount = 0 (manually call updateLockData) + await symmioBuildersNft.connect(admin).updateLockData( + tokenId, + 0, // amount = 0 + unlockAmount, // unlockingAmount stays the same + "FullLock", + ); + + // Fast-forward past cliff + await time.increase(Number(await symmioBuildersNftManager.cliffDuration()) + 1); + + // Complete vesting + await symmioBuildersNftManager.connect(user1).completeCliffAndStartVesting(0); + + // NFT should be burned + await expect(symmioBuildersNft.ownerOf(tokenId)).to.be.revertedWithCustomError( + symmioBuildersNft, + "ERC721NonexistentToken", + ); + }); + }); + + describe("SymmioLocker - batchUpdateLockData", function() { + it("should batch update lock data correctly", async () => { + await symmioToken.connect(admin).mint(await user1.getAddress(), ethers.parseEther("1000")); + await symmioToken.connect(user1).approve(await symmioBuildersNftManager.getAddress(), ethers.parseEther("1000")); + + const amount1 = ethers.parseEther("100"); + const amount2 = ethers.parseEther("200"); + + // Mint two NFTs + await symmioBuildersNftManager.connect(user1).mintAndLock(amount1, "NFT1"); + await symmioBuildersNftManager.connect(user1).mintAndLock(amount2, "NFT2"); + + const tokenId1 = 0; + const tokenId2 = 1; + + const updatedData = [ + { + amount: ethers.parseEther("300"), + unlockingAmount: ethers.parseEther("50"), + name: "Updated1", + lockTimestamp: (await time.latest()) - 1000, // or a fixed timestamp + }, + { + amount: ethers.parseEther("400"), + unlockingAmount: ethers.parseEther("75"), + name: "Updated2", + lockTimestamp: (await time.latest()) - 2000, + }, + ]; + + + // Grant SYNC_ROLE to user1 (assumes DEFAULT_ADMIN_ROLE or an admin is running test) + const SYNC_ROLE = await symmioBuildersNftManager.SYNC_ROLE(); + await symmioBuildersNftManager.grantRole(SYNC_ROLE, user1.address); + + // Call batch update + await symmioBuildersNftManager.connect(user1).batchUpdateLockData( + [tokenId1, tokenId2], + updatedData, + ); + + // Verify results + const data1 = await symmioBuildersNft.getLockData(tokenId1); + const data2 = await symmioBuildersNft.getLockData(tokenId2); + + expect(data1.amount).to.equal(updatedData[0].amount); + expect(data1.unlockingAmount).to.equal(updatedData[0].unlockingAmount); + expect(data1.name).to.equal(updatedData[0].name); + + expect(data2.amount).to.equal(updatedData[1].amount); + expect(data2.unlockingAmount).to.equal(updatedData[1].unlockingAmount); + expect(data2.name).to.equal(updatedData[1].name); + }); + + it("should revert if tokenIds and lockDatas length mismatch", async () => { + const SYNC_ROLE = await symmioBuildersNftManager.SYNC_ROLE(); + await symmioBuildersNftManager.grantRole(SYNC_ROLE, user1.address); + + await expect( + symmioBuildersNftManager.connect(user1).batchUpdateLockData( + [0], // 1 tokenId + [ + { + amount: ethers.parseEther("100"), + unlockingAmount: ethers.parseEther("50"), + name: "Mismatch", + lockTimestamp: (await time.latest()) - 1000, + + }, + { + amount: ethers.parseEther("200"), + unlockingAmount: ethers.parseEther("75"), + name: "Extra", + lockTimestamp: (await time.latest()) - 2000, + }, + ], // 2 lockDatas + ), + ).to.be.revertedWithCustomError(symmioBuildersNftManager, "LengthMismatch"); + }); + }); + + describe("SymmioBuildersNftManager - Admin Functions", function() { + let feeCollector1 = "0x0000000000000000000000000000000000000001"; + let feeCollector2 = "0x0000000000000000000000000000000000000002"; + describe("setMinLockAmount", function() { + it("should revert if amount = 0", async () => { + await expect(symmioBuildersNftManager.setMinLockAmount(0)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "ZeroAmount"); + }); + + it("should update minLockAmount and emit event", async () => { + const newAmount = ethers.parseEther("100"); + await expect(symmioBuildersNftManager.setMinLockAmount(newAmount)) + .to.emit(symmioBuildersNftManager, "MinLockAmountUpdated") + .withArgs(newAmount); + + expect(await symmioBuildersNftManager.minLockAmount()).to.equal(newAmount); + }); + }); + + describe("setCliffDuration", function() { + it("should revert if duration = 0", async () => { + await expect(symmioBuildersNftManager.setCliffDuration(0)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "InvalidDuration"); + }); + + it("should update cliffDuration and emit event", async () => { + const duration = 3600; + await expect(symmioBuildersNftManager.setCliffDuration(duration)) + .to.emit(symmioBuildersNftManager, "CliffDurationUpdated") + .withArgs(duration); + + expect(await symmioBuildersNftManager.cliffDuration()).to.equal(duration); + }); + }); + + describe("setVestingDuration", function() { + it("should revert if duration = 0", async () => { + await expect(symmioBuildersNftManager.setVestingDuration(0)) + .to.be.revertedWithCustomError(symmioBuildersNftManager, "InvalidDuration"); + }); + + it("should update vestingDuration and emit event", async () => { + const duration = 86400; + await expect(symmioBuildersNftManager.setVestingDuration(duration)) + .to.emit(symmioBuildersNftManager, "VestingDurationUpdated") + .withArgs(duration); + + expect(await symmioBuildersNftManager.vestingDuration()).to.equal(duration); + }); + }); + + describe("addFeeCollector / removeFeeCollector", function() { + const tokenId = 0; + + it("should add fee collectors and emit events", async () => { + await expect(symmioBuildersNftManager.addFeeCollector(tokenId, [feeCollector1, feeCollector2])) + .to.emit(symmioBuildersNftManager, "FeeCollectorAdded").withArgs(tokenId, feeCollector1) + .and.to.emit(symmioBuildersNftManager, "FeeCollectorAdded").withArgs(tokenId, feeCollector2); + + const collectors = await symmioBuildersNftManager.getTokenFeeCollectors(tokenId); + expect(collectors).to.include(feeCollector1); + expect(collectors).to.include(feeCollector2); + }); + + it("should remove fee collector and emit event", async () => { + await symmioBuildersNftManager.addFeeCollector(tokenId, [feeCollector1]); + await expect(symmioBuildersNftManager.removeFeeCollector(tokenId, feeCollector1)) + .to.emit(symmioBuildersNftManager, "FeeCollectorRemoved").withArgs(tokenId, feeCollector1); + + const collectors = await symmioBuildersNftManager.getTokenFeeCollectors(tokenId); + expect(collectors).to.not.include(feeCollector1); + }); + }); + + describe("role restrictions", function() { + it("non-setter should be reverted", async () => { + await expect(symmioBuildersNftManager.connect(user1).setMinLockAmount(100)).to.be.rejectedWith("AccessControl"); + }); + }); + }); + + describe("SymmioBuildersNftManager - View Functions", function() { + let tokenId = 0; + beforeEach(async function() { + await symmioBuildersNft.mint(await admin.getAddress(), ethers.parseEther("1000"), "Yours"); + + // set cliff duration + await symmioBuildersNftManager.setCliffDuration(1000); // e.g., 1000s + }); + + async function createUnlock(amount: bigint) { + const tx = await symmioBuildersNftManager.connect(admin).initiateUnlock(tokenId, amount); + await tx.wait(); + return tx; + } + + describe("getUnlockedRequests", function() { + it("should return empty array if no unlocks", async () => { + const requests = await symmioBuildersNftManager.getUnlockedRequests(tokenId, 0, 10, 10); + expect(requests.length).to.equal(0n); + }); + + it("should return unlock requests with correct pagination", async () => { + // Create 3 unlocks + await createUnlock(ethers.parseEther("100")); + await createUnlock(ethers.parseEther("200")); + await createUnlock(ethers.parseEther("300")); + + // Fetch first 2 + const reqs1 = await symmioBuildersNftManager.getUnlockedRequests(tokenId, 0, 2, 10); + expect(reqs1.length).to.equal(2n); + expect(reqs1[0].amount).to.equal(ethers.parseEther("100")); + expect(reqs1[1].amount).to.equal(ethers.parseEther("200")); + + // Fetch with size limit = 1 + const reqs2 = await symmioBuildersNftManager.getUnlockedRequests(tokenId, 0, 3, 1); + expect(reqs2.length).to.equal(1n); + expect(reqs2[0].amount).to.equal(ethers.parseEther("100")); + }); + }); + + describe("getCliffEndTime", function() { + it("should revert if unlock not found", async () => { + await expect(symmioBuildersNftManager.getCliffEndTime(0)).to.be.revertedWithCustomError( + symmioBuildersNftManager, + "UnlockNotFound", + ); + }); + + it("should return correct cliff end time", async () => { + const tx = await createUnlock(ethers.parseEther("100")); + + const block = await ethers.provider.getBlock(tx.blockNumber!); + + const cliffEnd = await symmioBuildersNftManager.getCliffEndTime(0); + expect(cliffEnd).to.equal(block!.timestamp + 1000); + }); + }); + + describe("isCliffPassed", function() { + it("should revert if unlock not found", async () => { + await expect(symmioBuildersNftManager.isCliffPassed(0)).to.be.revertedWithCustomError( + symmioBuildersNftManager, + "UnlockNotFound", + ); + }); + + it("should return false before cliff", async () => { + await createUnlock(ethers.parseEther("100")); + expect(await symmioBuildersNftManager.isCliffPassed(0)).to.equal(false); + }); + + it("should return true after cliff time passes", async () => { + await createUnlock(ethers.parseEther("100")); + await ethers.provider.send("evm_increaseTime", [2000]); // move forward + await ethers.provider.send("evm_mine"); + + expect(await symmioBuildersNftManager.isCliffPassed(0)).to.equal(true); + }); + }); + }); +} diff --git a/tests/vestingV2.behavior.ts b/tests/vestingV2.behavior.ts new file mode 100644 index 0000000..965441d --- /dev/null +++ b/tests/vestingV2.behavior.ts @@ -0,0 +1,742 @@ +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers, network, upgrades } from "hardhat"; +import { Symmio, SymmVestingV2, VestingPlanOps__factory } from "../typechain-types"; +import { initializeFixture, RunContext } from "./Initialize.fixture"; +import { Signer } from "ethers"; +import { e } from "../utils"; + +export function ShouldBehaveLikeVestingV2() { + let context: RunContext; + let symmVesting: SymmVestingV2; + let vestingPlanOps: VestingPlanOps__factory; + let admin: Signer, user1: Signer; + let symmToken: Symmio; + + beforeEach(async () => { + context = await loadFixture(initializeFixture); + symmVesting = await context.vesting; + vestingPlanOps = await ethers.getContractFactory("VestingPlanOps"); + symmToken = context.symmioToken; + admin = context.signers.admin; + user1 = context.signers.user1; + }); + + describe("__vesting_init", () => { + it("Should grant the admin role to the deployer", async () => { + for (const role of [ + await symmVesting.DEFAULT_ADMIN_ROLE(), + await symmVesting.SETTER_ROLE(), + await symmVesting.PAUSER_ROLE(), + await symmVesting.UNPAUSER_ROLE(), + await symmVesting.OPERATOR_ROLE(), + ]) { + const hasRole = await symmVesting.hasRole(role, context.signers.admin.address); + await expect(hasRole).to.be.true; + } + }); + }); + + describe("setupVestingPlans", () => { + it("Should fail if users and amount arrays mismatch", async () => { + const users = [await context.signers.user1.getAddress(), await context.signers.user2.getAddress()]; + const amounts = ["1000"]; + + await expect(symmVesting.setupVestingPlans(await context.symmioToken.getAddress(), "0", "0", users, amounts)).to.be.revertedWithCustomError( + symmVesting, + "MismatchArrays", + ); + }); + + // it("Should fail if vestingPlan setup before", async () => { + // const users = [await context.signers.user1.getAddress()] + // const amounts = ["1000"] + // + // await symmVesting.setupVestingPlans(await context.symmioToken.getAddress(), "0", "0", users, amounts) + // await expect(symmVesting.setupVestingPlans(await context.symmioToken.getAddress(), "0", "0", users, amounts)).to.be.revertedWithCustomError( + // vestingPlanOps, + // "AlreadySetup", + // ) + // }) + + it("Should setup vestingPlan successfully", async () => { + const symmioToken = await context.symmioToken.getAddress(); + const user1 = await context.signers.user1.getAddress(); + + const users = [user1]; + const amounts = ["1000"]; + + const oldTotalVesting = await symmVesting.totalVested(symmioToken); + + // Get the vesting plan count before creating the plan (it will be used as the planId) + const planId = await symmVesting.userVestingPlanCount(symmioToken, user1); + + await expect(await symmVesting.setupVestingPlans(symmioToken, "0", "0", users, amounts)).to.be.not.reverted; + + // Now use the original planId to fetch the plan + const plan = await symmVesting.vestingPlans(symmioToken, user1, planId); + + const newTotalVesting = await symmVesting.totalVested(symmioToken); + + await expect(newTotalVesting).to.be.equal(oldTotalVesting + amounts[0]); + + await expect(plan.startTime).to.be.equal("0"); + await expect(plan.endTime).to.be.equal("0"); + await expect(plan.amount).to.be.equal(amounts[0]); + await expect(plan.claimedAmount).to.be.equal(0); + }); + }); + + // describe("claimUnlockedToken", () => { + // let planId: BigInt; + // beforeEach(async () => { + // await context.symmioToken.connect(context.signers.admin).mint(await symmVesting.getAddress(), 5000); + // const users = [await context.signers.user1.getAddress()]; + // const amounts = ["1000"]; + // const now = new Date(); + // const startTime = Math.floor(now.getTime() / 1000); + // planId = await symmVesting.userVestingPlanCount(await context.symmioToken.getAddress(), await context.signers.user1.getAddress()); + // + // now.setMonth(now.getMonth() + 9); + // const endTime = Math.floor(now.getTime() / 1000); + // + // await symmVesting.setupVestingPlans(await context.symmioToken.getAddress(), startTime, endTime, users, amounts); + // }); + // + // it("Should unlockedAmount be zero before vesting starts", async () => { + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // const latestBlock = await ethers.provider.getBlock("latest"); + // const safeTimestamp = Number(plan.startTime) - 100; + // + // if (safeTimestamp <= (latestBlock?.timestamp ?? 0)) { + // await network.provider.send("evm_setNextBlockTimestamp", [(latestBlock?.timestamp ?? 0) + 1]); + // } else { + // await network.provider.send("evm_setNextBlockTimestamp", [safeTimestamp]); + // } + // + // await network.provider.send("evm_mine"); + // + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(0); + // }); + // + // it("Should unlockedAmount be zero at the exact start time", async () => { + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // const latestBlock = await ethers.provider.getBlock("latest"); + // const safeTimestamp = Number(plan.startTime); + // + // if (safeTimestamp <= (latestBlock?.timestamp ?? 0)) { + // await network.provider.send("evm_setNextBlockTimestamp", [(latestBlock?.timestamp ?? 0) + 1]); + // } else { + // await network.provider.send("evm_setNextBlockTimestamp", [safeTimestamp]); + // } + // + // await network.provider.send("evm_mine"); + // + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(0); + // }); + // + // it("Should unlockedAmount be partial during the vesting period", async () => { + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // const midTime = Math.floor(Number((plan.startTime + plan.endTime) / BigInt(2))); + // + // await network.provider.send("evm_setNextBlockTimestamp", [midTime]); // half of vesting lock + // await network.provider.send("evm_mine"); + // + // const expectedUnlocked = Math.floor(Number((BigInt(1000) * (BigInt(midTime) - plan.startTime)) / (plan.endTime - plan.startTime))); + // + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(expectedUnlocked); + // }); + // + // it("Should unlockedAmount be the full amount at the exact end time", async () => { + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // await network.provider.send("evm_setNextBlockTimestamp", [Number(plan.endTime)]); + // await network.provider.send("evm_mine"); + // + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(1000); + // }); + // + // it("Should claimUnlockedToken successfully", async () => { + // // @ts-ignore + // let plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // await network.provider.send("evm_setNextBlockTimestamp", [Number(plan.endTime)]); + // await network.provider.send("evm_mine"); + // + // const oldTotalVested = await symmVesting.totalVested(context.symmioToken); + // const oldClaimedAmount = plan.claimedAmount; + // const oldContractBalance = await context.symmioToken.balanceOf(symmVesting); + // const oldUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // // @ts-ignore + // await expect(await symmVesting.connect(context.signers.user1).claimUnlockedToken(await context.symmioToken.getAddress(), planId)).to.be.not.reverted; + // // @ts-ignore + // plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // + // const newTotalVested = await symmVesting.totalVested(context.symmioToken); + // const newClaimedAmount = plan.claimedAmount; + // const newContractBalance = await context.symmioToken.balanceOf(symmVesting); + // const newUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // await expect(newTotalVested).to.be.equal(oldTotalVested - BigInt(1000)); + // await expect(newClaimedAmount).to.be.equal(oldClaimedAmount + BigInt(1000)); + // await expect(newContractBalance).to.be.equal(oldContractBalance - BigInt(1000)); + // await expect(newUserBalance).to.be.equal(oldUserBalance + BigInt(1000)); + // }); + // }); + // + // describe("claimLockedToken", () => { + // let planId: BigInt; + // beforeEach(async () => { + // await context.symmioToken.connect(context.signers.admin).mint(await symmVesting.getAddress(), 5000); + // const users = [await context.signers.user1.getAddress()]; + // const amounts = ["1000"]; + // const now = new Date(); + // const startTime = Math.floor(now.getTime() / 1000); + // planId = await symmVesting.userVestingPlanCount(await context.symmioToken.getAddress(), await context.signers.user1.getAddress()); + // + // now.setMonth(now.getMonth() + 9); + // const endTime = Math.floor(now.getTime() / 1000); + // + // await symmVesting.setupVestingPlans(await context.symmioToken.getAddress(), startTime, endTime, users, amounts); + // }); + // + // it("Should fail if amount be greater than lockedAmount", async () => { + // // @ts-ignore + // await expect(symmVesting.connect(context.signers.user1).claimLockedToken(context.symmioToken, planId, 1001)).to.be.revertedWithCustomError(symmVesting, "InvalidAmount"); + // }); + // + // it("Should not revert when claiming locked tokens within allowed amount", async () => { + // // @ts-ignore + // await expect(symmVesting.connect(context.signers.user1).claimLockedToken(context.symmioToken, planId, 1000)).to.be.not.reverted; + // }); + // + // it("Should decrease total vested amount after claiming", async () => { + // const oldTotalVested = await symmVesting.totalVested(context.symmioToken); + // + // // @ts-ignore + // await symmVesting.connect(context.signers.user1).claimLockedToken(context.symmioToken, planId, 1000); + // + // const newTotalVested = await symmVesting.totalVested(context.symmioToken); + // await expect(newTotalVested).to.be.equal(oldTotalVested - BigInt(1000)); + // }); + // + // it("Should distribute claimed amount correctly", async () => { + // const oldPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const oldUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // // @ts-ignore + // await symmVesting.connect(context.signers.user1).claimLockedToken(context.symmioToken, planId, 1000); + // + // const newPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const newUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // await expect(newPenaltyContractBalance).to.be.equal(oldPenaltyContractBalance + BigInt(500)); + // await expect(newUserBalance).to.be.equal(oldUserBalance + BigInt(500)); + // }); + // + // it("Should allow user to claim locked token by percentage", async () => { + // const oldPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const oldUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // // @ts-ignore + // await symmVesting.connect(context.signers.user1).claimLockedTokenByPercentage(context.symmioToken, planId, e(0.5)); + // + // const newPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const newUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // await expect(newPenaltyContractBalance).to.be.equal(oldPenaltyContractBalance + BigInt(250)); + // await expect(newUserBalance).to.be.equal(oldUserBalance + BigInt(250)); + // }); + // + // it("Should allow admin to claim locked token for user", async () => { + // const oldPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const oldUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // // @ts-ignore + // await symmVesting.connect(admin).claimLockedTokenFor(symmToken, user1, planId, 1000); + // + // const newPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const newUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // await expect(newPenaltyContractBalance).to.be.equal(oldPenaltyContractBalance + BigInt(500)); + // await expect(newUserBalance).to.be.equal(oldUserBalance + BigInt(500)); + // }); + // + // it("Should allow admin to claim locked token for user by percentage", async () => { + // const oldPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const oldUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // // @ts-ignore + // await symmVesting.connect(context.signers.admin).claimLockedTokenForByPercentage(context.symmioToken, user1, planId, e(0.5)); + // + // const newPenaltyContractBalance = await context.symmioToken.balanceOf(await symmVesting.lockedClaimPenaltyReceiver()); + // const newUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // await expect(newPenaltyContractBalance).to.be.equal(oldPenaltyContractBalance + BigInt(250)); + // await expect(newUserBalance).to.be.equal(oldUserBalance + BigInt(250)); + // }); + // + // it("Should reset claimed amount to zero after claiming", async () => { + // // @ts-ignore + // await symmVesting.connect(context.signers.user1).claimLockedToken(context.symmioToken, planId, 1000); + // + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // await expect(plan.claimedAmount).to.be.equal(0); + // }); + // }); + // + // describe("resetVestingPlans", () => { + // let planId: BigInt; + // beforeEach(async () => { + // await context.symmioToken.connect(context.signers.admin).mint(await symmVesting, 5000); + // + // const users = [await context.signers.user1.getAddress()]; + // const amounts = ["1000"]; + // const now = new Date(); + // const startTime = Math.floor(now.getTime() / 1000); + // planId = await symmVesting.userVestingPlanCount(await context.symmioToken.getAddress(), await context.signers.user1.getAddress()); + // + // now.setMonth(now.getMonth() + 9); + // const endTime = Math.floor(now.getTime() / 1000); + // + // await symmVesting.setupVestingPlans(await context.symmioToken.getAddress(), startTime, endTime, users, amounts); + // }); + // + // it("Should reset vesting plan successfully", async () => { + // const user = await context.signers.user1.getAddress(); + // const newAmount = BigInt(1500); + // const token = await context.symmioToken.getAddress(); + // + // // @ts-ignore + // const planBefore = await symmVesting.vestingPlans(token, user, planId); + // const totalVestedBefore = await symmVesting.totalVested(token); + // + // // @ts-ignore + // await expect(symmVesting.connect(context.signers.admin).resetVestingPlans(token, [user], [planId], [newAmount])) + // .to.emit(symmVesting, "VestingPlanReset") + // .withArgs(token, user, 0, newAmount); + // + // // @ts-ignore + // const planAfter = await symmVesting.vestingPlans(token, user, planId); + // const totalVestedAfter = await symmVesting.totalVested(token); + // + // await expect(planAfter.amount).to.equal(newAmount); + // await expect(planAfter.claimedAmount).to.equal(0); + // await expect(totalVestedAfter).to.equal(totalVestedBefore - planBefore.amount + newAmount); + // }); + // + // it("Should fail if users and amounts arrays have different lengths", async () => { + // const user = await context.signers.user1.getAddress(); + // const token = await context.symmioToken.getAddress(); + // + // // @ts-ignore + // await expect(symmVesting.connect(context.signers.admin).resetVestingPlans(token, [user], [planId], [])).to.be.revertedWithCustomError( + // symmVesting, + // "MismatchArrays", + // ); + // }); + // + // + // it("Should claim unlocked tokens before resetting", async () => { + // const user = await context.signers.user1.getAddress(); + // const token = await context.symmioToken.getAddress(); + // const newAmount = 1200; + // + // // @ts-ignore + // await expect(symmVesting.connect(context.signers.admin).resetVestingPlans(token, [user], [planId], [newAmount])).to.not.be.reverted; + // + // // @ts-ignore + // const planAfter = await symmVesting.vestingPlans(token, user, planId); + // await expect(planAfter.claimedAmount).to.equal(0); + // }); + // }); + // + // describe("modifiers", () => { + // it("should allow PAUSER_ROLE to pause and unpase the contract", async () => { + // await symmVesting.connect(admin).pause(); + // await expect(await symmVesting.paused()).to.be.true; + // + // let planId = await symmVesting.userVestingPlanCount(await context.symmioToken.getAddress(), await context.signers.user1.getAddress()); + // + // await expect(symmVesting.connect(admin).resetVestingPlans( + // await symmToken.getAddress(), [await user1.getAddress()], [planId], [e(1)], + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).setupVestingPlans( + // await symmToken.getAddress(), + // Math.floor(Date.now() / 1000), + // Math.floor(Date.now() / 1000) + 3600, + // [await user1.getAddress()], + // [e(1)], + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).setupVestingPlans( + // await symmToken.getAddress(), + // Math.floor(Date.now() / 1000), + // Math.floor(Date.now() / 1000) + 3600, + // [await user1.getAddress()], + // [e(1)], + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).claimUnlockedToken( + // await symmToken.getAddress(), planId, + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).claimUnlockedTokenFor( + // await symmToken.getAddress(), + // await user1.getAddress(), + // planId, + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).claimLockedToken( + // await symmToken.getAddress(), + // planId, + // await user1.getAddress(), + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).claimLockedTokenByPercentage( + // await symmToken.getAddress(), + // planId, + // e(5e-1), + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).claimLockedTokenFor( + // await symmToken.getAddress(), + // await user1.getAddress(), + // planId, + // e(10), + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(admin).claimLockedTokenForByPercentage( + // await symmToken.getAddress(), + // await user1.getAddress(), + // planId, + // e(10), + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await expect(symmVesting.connect(user1).addLiquidity( + // e(1), + // 0, + // 0, + // planId, + // )).to.be.revertedWithCustomError(symmVesting, "EnforcedPause"); + // + // await symmVesting.connect(admin).unpause(); + // await expect(await symmVesting.paused()).to.be.false; + // + // await expect(symmVesting.connect(admin).resetVestingPlans( + // await symmToken.getAddress(), [await user1.getAddress()], [planId], [e(1)], + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).setupVestingPlans( + // await symmToken.getAddress(), + // Math.floor(Date.now() / 1000), + // Math.floor(Date.now() / 1000) + 3600, + // [await user1.getAddress()], + // [e(1)], + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).setupVestingPlans( + // await symmToken.getAddress(), + // Math.floor(Date.now() / 1000), + // Math.floor(Date.now() / 1000) + 3600, + // [await user1.getAddress()], + // [e(1)], + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).claimUnlockedToken( + // await symmToken.getAddress(), planId, + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).claimUnlockedTokenFor( + // await symmToken.getAddress(), + // await user1.getAddress(), + // planId, + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).claimLockedToken( + // await symmToken.getAddress(), + // planId, + // await user1.getAddress(), + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).claimLockedTokenByPercentage( + // await symmToken.getAddress(), + // planId, + // e(5e-1), + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).claimLockedTokenFor( + // await symmToken.getAddress(), + // await user1.getAddress(), + // planId, + // e(10), + // )).to.be.ok; + // + // await expect(symmVesting.connect(admin).claimLockedTokenForByPercentage( + // await symmToken.getAddress(), + // await user1.getAddress(), + // planId, + // e(10), + // )).to.be.ok; + // + // await expect(symmVesting.connect(user1).addLiquidity( + // e(1), + // 0, + // 0, + // planId, + // )).to.be.ok; + // }); + // + // it("should revert when initialize method is from nonInitializer/constructor method", + // async () => { + // const zeroAddress = "0x0000000000000000000000000000000000000000"; + // await expect(symmVesting.connect(admin).initialize(admin, admin, + // zeroAddress, zeroAddress, zeroAddress, zeroAddress, zeroAddress, zeroAddress, zeroAddress)) + // .to.be.reverted; + // const adminAdress = await admin.getAddress(); + // await expect(symmVesting.connect(admin).__vesting_init(adminAdress, adminAdress, adminAdress)) + // .to.be.reverted; + // }); + // + // it("should fail when zero is passed as address to symmVesting initialize method", async () => { + // const VestingPlanOps = await ethers.getContractFactory("VestingPlanOps"); + // const vestingPlanOps = await VestingPlanOps.deploy(); + // await vestingPlanOps.waitForDeployment(); + // + // const zeroAddress = "0x0000000000000000000000000000000000000000"; + // const nonZeroAddress = "0x0000000000000000000000000000000000000001"; + // + // const VestingFactory = await ethers.getContractFactory("SymmVesting", { + // libraries: { + // VestingPlanOps: await vestingPlanOps.getAddress(), + // }, + // }); + // + // await expect(upgrades.deployProxy(VestingFactory, [zeroAddress, nonZeroAddress, + // nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, zeroAddress, + // nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, nonZeroAddress, + // zeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, nonZeroAddress, + // nonZeroAddress, zeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, nonZeroAddress, + // nonZeroAddress, nonZeroAddress, zeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, nonZeroAddress, + // nonZeroAddress, nonZeroAddress, nonZeroAddress, zeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, nonZeroAddress, + // nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, zeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, nonZeroAddress, + // nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, zeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // + // await expect(upgrades.deployProxy(VestingFactory, [nonZeroAddress, nonZeroAddress, + // nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, nonZeroAddress, zeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "initialize", + // })).to.be.revertedWithCustomError(symmVesting, "ZeroAddress"); + // }); + // + // it("should fail when zero is passed as address to vesting initialize method", async () => { + // const VestingPlanOps = await ethers.getContractFactory("VestingPlanOps"); + // const vestingPlanOps = await VestingPlanOps.deploy(); + // await vestingPlanOps.waitForDeployment(); + // + // const zeroAddress = "0x0000000000000000000000000000000000000000"; + // const nonZeroAddress = "0x0000000000000000000000000000000000000001"; + // + // const Vesting = await ethers.getContractFactory("SymmVesting", { + // libraries: { + // VestingPlanOps: await vestingPlanOps.getAddress(), + // }, + // }); + // await expect(upgrades.deployProxy(Vesting, [zeroAddress, nonZeroAddress, nonZeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "__vesting_init", + // })).to.be.revertedWithCustomError(symmVesting, "NotInitializing"); + // + // await expect(upgrades.deployProxy(Vesting, [nonZeroAddress, nonZeroAddress, zeroAddress], { + // unsafeAllow: ["external-library-linking"], + // initializer: "__vesting_init", + // })).to.be.revertedWithCustomError(symmVesting, "NotInitializing"); + // }); + // }); + // + // describe("Role management", () => { + // it("should allow calling methods just to the ones who have the required role", async () => { + // let planId = await symmVesting.userVestingPlanCount(await context.symmioToken.getAddress(), await context.signers.user1.getAddress()); + // + // await expect(symmVesting.connect(user1).pause()).to.be.revertedWithCustomError(symmVesting, "AccessControlUnauthorizedAccount"); + // await expect(symmVesting.connect(user1).unpause()).to.be.revertedWithCustomError(symmVesting, "AccessControlUnauthorizedAccount"); + // + // await expect(symmVesting.connect(user1).resetVestingPlans(symmToken, [await user1.getAddress()], [planId], [e(1)])) + // .to.be.revertedWithCustomError(symmVesting, "AccessControlUnauthorizedAccount"); + // await expect(symmVesting.connect(user1).setupVestingPlans( + // await symmToken.getAddress(), + // Math.floor(Date.now() / 1000), + // Math.floor(Date.now() / 1000) + 3600, + // [await user1.getAddress()], + // [e(1)], + // )).to.be.revertedWithCustomError(symmVesting, "AccessControlUnauthorizedAccount"); + // + // await expect(symmVesting.connect(user1).claimUnlockedTokenFor(symmToken, user1, planId)) + // .to.be.revertedWithCustomError(symmVesting, "AccessControlUnauthorizedAccount"); + // + // await expect(symmVesting.connect(user1).claimLockedTokenFor(symmToken, user1, planId, e(1))) + // .to.be.revertedWithCustomError(symmVesting, "AccessControlUnauthorizedAccount"); + // + // await expect(symmVesting.connect(user1).claimLockedTokenForByPercentage(symmToken, user1, planId, e(0.5))) + // .to.be.revertedWithCustomError(symmVesting, "AccessControlUnauthorizedAccount"); + // }); + // }); + // + // describe("multiClaimUnlockedToken", () => { + // let lastPlanId: BigInt; + // const amounts = ["1000", "2000", "3000"]; + // let now :number; + // let startTime :number; + // let endTime :number; + // beforeEach(async () => { + // await context.symmioToken.connect(context.signers.admin).mint(await symmVesting.getAddress(), 10000); + // const users = [await context.signers.user1.getAddress(), await context.signers.user1.getAddress(), await context.signers.user1.getAddress()]; + // now = await ethers.provider.getBlock("latest").then(block => block?.timestamp ?? 0) + // startTime = now; + // endTime = now + 36000; + // await symmVesting.setupVestingPlans(await context.symmioToken.getAddress(), startTime, endTime, users, amounts); + // + // lastPlanId = await symmVesting.userVestingPlanCount(await context.symmioToken.getAddress(), await context.signers.user1.getAddress()); + // }); + // + // it("Should unlockedAmount be zero before vesting starts", async () => { + // for (let planId = 0; planId < lastPlanId.valueOf(); planId++) { + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // const latestBlock = await ethers.provider.getBlock("latest"); + // const safeTimestamp = Number(plan.startTime) - 100; + // + // if (safeTimestamp <= (latestBlock?.timestamp ?? 0)) { + // await network.provider.send("evm_setNextBlockTimestamp", [(latestBlock?.timestamp ?? 0) + 1]); + // } else { + // await network.provider.send("evm_setNextBlockTimestamp", [safeTimestamp]); + // } + // + // await network.provider.send("evm_mine"); + // + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(0); + // } + // }); + // + // it("Should unlockedAmount be zero at the exact start time", async () => { + // for (let planId = 0; planId < lastPlanId.valueOf(); planId++) { + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // const latestBlock = await ethers.provider.getBlock("latest"); + // const safeTimestamp = Number(plan.startTime); + // + // if (safeTimestamp <= (latestBlock?.timestamp ?? 0)) { + // await network.provider.send("evm_setNextBlockTimestamp", [(latestBlock?.timestamp ?? 0) + 1]); + // } else { + // await network.provider.send("evm_setNextBlockTimestamp", [safeTimestamp]); + // } + // + // await network.provider.send("evm_mine"); + // + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(0); + // } + // }); + // + // it("Should unlockedAmount be partial during the vesting period", async () => { + // let current_time = now + 12000; + // await network.provider.send("evm_setNextBlockTimestamp", [current_time]); // half of vesting lock + // await network.provider.send("evm_mine"); + // for (let planId = 0; planId < lastPlanId.valueOf(); planId++) { + // // @ts-ignore + // const plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // + // const expectedUnlocked = Math.floor(Number((BigInt(amounts[planId]) * (BigInt(current_time) - plan.startTime)) / (plan.endTime - plan.startTime))); + // + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(expectedUnlocked); + // } + // }); + // + // it("Should unlockedAmount be the full amount at the exact end time", async () => { + // await network.provider.send("evm_setNextBlockTimestamp", [endTime]); // half of vesting lock + // await network.provider.send("evm_mine"); + // for (let planId = 0; planId < lastPlanId.valueOf(); planId++) { + // // @ts-ignore + // await expect(await symmVesting.getUnlockedAmountForPlan(await context.signers.user1.getAddress(), context.symmioToken, planId)).to.be.equal(BigInt(amounts[planId])); + // } + // }); + // + // it("Should claimUnlockedToken successfully", async () => { + // await network.provider.send("evm_setNextBlockTimestamp", [endTime]); + // await network.provider.send("evm_mine"); + // for (let planId = 0; planId < lastPlanId.valueOf(); planId++) { + // // @ts-ignore + // let plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // + // const oldTotalVested = await symmVesting.totalVested(context.symmioToken); + // const oldClaimedAmount = plan.claimedAmount; + // const oldContractBalance = await context.symmioToken.balanceOf(symmVesting); + // const oldUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // // @ts-ignore + // await expect(await symmVesting.connect(context.signers.user1).claimUnlockedToken(await context.symmioToken.getAddress(), planId)).to.be.not.reverted; + // // @ts-ignore + // plan = await symmVesting.vestingPlans(context.symmioToken, await context.signers.user1.getAddress(), planId); + // + // const newTotalVested = await symmVesting.totalVested(context.symmioToken); + // const newClaimedAmount = plan.claimedAmount; + // const newContractBalance = await context.symmioToken.balanceOf(symmVesting); + // const newUserBalance = await context.symmioToken.balanceOf(context.signers.user1); + // + // await expect(newTotalVested).to.be.equal(oldTotalVested - BigInt(amounts[planId])); + // await expect(newClaimedAmount).to.be.equal(oldClaimedAmount + BigInt(amounts[planId])); + // await expect(newContractBalance).to.be.equal(oldContractBalance - BigInt(amounts[planId])); + // await expect(newUserBalance).to.be.equal(oldUserBalance + BigInt(amounts[planId])); + // } + // }); + // }); +} diff --git a/user_available_symm.json b/user_available_symm.json new file mode 100644 index 0000000..9c86a6e --- /dev/null +++ b/user_available_symm.json @@ -0,0 +1,3200 @@ +{ + "Users": [ + "0x02470bc5208ca9dd9108765563e9fc63c7ecb003", + "0x029829c4c83dc42245c87f01b69486f2606ef5a4", + "0x068590446d70a64f5babd55a1ac62456531e5b87", + "0x088bea4e439459db146b9e7a974472a5cbd54ba8", + "0x08c3add437d0e3b86586d69220b725b959092006", + "0x0c6501f3b904287e5ee6ac3e1217e2ddde56903c", + "0x0d2bab6899c0a058b0f3162f656660a2f41a7712", + "0x0d7661a7b7a89b40b6db128900557e4f1d1b3789", + "0x0e4aae38e62bc9725a3d895826199d2e2b18ef16", + "0x136275c4141a2e6f8ccb25d9b512bf3038204a46", + "0x136ea1a4965fbe2587cfdb26d08c7afdee0da608", + "0x1418eb962263c8ecfa205e3431db617cf6e8364d", + "0x1761aa7c32728b3af65d1d0bec60ce227c10e409", + "0x179330072d6eea791da09cf8044e4ae29c0f064c", + "0x18752fb8f60636e909ee9ad582b08dd9acc77e01", + "0x1c4d2a83ec362100712b4beca1d727e25d0f1e12", + "0x1d9989fe715c118c74a54db24cc62c5bd6d514df", + "0x1e73a9ae4bd6d8db3959f1fbba0eb02773d4b3b4", + "0x21ff5ffb6110f8ba001332f2aa2e7077c68240b9", + "0x24a7e2f78c3ac4181996e415f7dbea1a71994952", + "0x25ca93c584756d674d6fe6d1e241a89791b18df3", + "0x2632d0fd0d1f53efdd60ca6a0c1fae0fdc37974d", + "0x28867de9098ab9ed4e6aa7c7fc51be553252d52a", + "0x28bf500861721a114d19fa288343c00cd0c2e73d", + "0x29c728ad2e1e3ab9a6005429ac6b7fda6adffb9d", + "0x2a6d4ab31b2325f604f3254f7ab39cb54cf03f94", + "0x2c4f7d3cf2e0d44b0aadabf767033782f4045bf2", + "0x2c6bb6ee977153f2d863e437a0f657d7d2533517", + "0x2c7b847278e02c1e381edd68f7c571a7f7fe9323", + "0x2e9cef5d459f013bc6a48b2e636721fcf3f8939f", + "0x2f45724d7e384b38d5c97206e78470544304887f", + "0x3359e443be74d653421af3f8d91dfeba09665ecf", + "0x348d57009a2dfc3ecf1dd412154698c2107bcae9", + "0x36dbfd5e39d5f45eb7ff4ab7bd1167bf243f2151", + "0x3719d1be534d09c1a01f4bf61eac18359070962a", + "0x3af6e4903d7eb6a88ba5ee41f2281d1221ba3928", + "0x3b57bd130baf955930feb24e4787442327c6fc17", + "0x3ece735ff9724354bc2922f5910b32901ea277e4", + "0x44613301f1e32fd76aeb6097f6fa0b59af66710b", + "0x489362c11acbeffdf713e68c834d64102d3f68ec", + "0x4c26e1ecb596a76a5c264b06751d71eff92f4898", + "0x4d82d35f75d0ca320d4eaf277ab56181ca5b0f91", + "0x4eb80511eea33f3b165fb3093ae2cf646c4335f4", + "0x519a5e2f4b33c83d8fd2fc59d994bb7a94895ef0", + "0x5236df6802994bae798d07bc4c10e9f915b70852", + "0x53d630f1c89d6ab1ba904a9de0dba21a11ae5bc0", + "0x54aabae177e11e37fbc23f525d6542f7f6f7a283", + "0x5539cb45393bc597c7f57ba0b49fd313dbd054f7", + "0x55759be298147ecad66fab2708842a294ab97be8", + "0x56351bd33bdae0981e45661666a34bc315aaad60", + "0x56f2c4cb8518d0a4ec749593df574f7597a36140", + "0x573d83675ad319dfde01abe3fa1f9a5a4ef0a531", + "0x58cf11aa4269e93d548a582d699a5c2a33f21c30", + "0x5a7a2543896080e517e2233fd2295448d016ed14", + "0x5bcbaba9704f625bdb7eb1d8a3c305737916e6ff", + "0x5bcf371e0b0725d51e66e3f9eeafef864675a785", + "0x5ed97928654f6a1455006478116fb6b92f693c5f", + "0x602cf79655a085e735327a8de9a44b36579bb081", + "0x622305675d32117c0cb431101f557f4400f83a7e", + "0x62c6ede90b1d4ce89bcd0224dd92dc80c983836e", + "0x65268d4fb73544b6056fa4f4bf20fc90e0b12a80", + "0x6683f947074723c40f3bf9e3c4ffce45c3e9ae08", + "0x6716a94fa26ea5e2de2aabd6d91b3c1d509d7eb0", + "0x6720ddf5cd112a57ea30f1f16b70f6f213af71e8", + "0x69258d1ed30a0e5971992921cb5787b9c7a2909d", + "0x6a018ad51de844243ce7fb743c3e5f4481df6858", + "0x6c022e721f0a619f760ae654dabb872c1f01e3bd", + "0x6d4854423b82e1509978df9573508904b38c8998", + "0x6f6830ba0571887fe2b5ce0be27814fffc4029da", + "0x7059300dc484c3c7578e1505df99699298e4617c", + "0x74a358a47ce5244a0829902a207b68dd8c5f4bdd", + "0x7c3d7f5182d6e875237a29256234dcd49594f321", + "0x7cab04c9ffa76ba94448476cdad4ee4e6151eeb0", + "0x7d51443a9e555883c05b8a99ecddb919ae79d3a6", + "0x7dfc44f577883bed0ecdb9966530332af6b5faba", + "0x800f965fe45d52a6f8d6a6ac7d976c69319ba8e7", + "0x81c3c22e4d7f5ca219b6ee8d45be04acf5b23654", + "0x8250a7f9fca9c33e68eead8c05c1d58c8fd70d13", + "0x83fb975f8cf383d079ecc06e89c380ad26ab4f1e", + "0x85204a5e932b69455822033f33e378dff4bb8960", + "0x89b6cab89f063a26e406eeab769e63451b386829", + "0x89b993ee71d33ddbcbc7f492f0c2dfe8a3a84d75", + "0x8c4eaa0559a78ac453ea9570297f5fc628c8b592", + "0x8c8a90dec29597399f36375e10b01bee1c5f7888", + "0x8e29d0e2ca8e92a9f27192616e2e9f170fd2a035", + "0x8e64d123e73d9e0f10517c56c0a9bf2bab83040a", + "0x8ec5d230232f6d48353106cf8a911bb2fe435394", + "0x8f25818ff64be1abee91abca70c306b33b602062", + "0x905c1ca2ac32ee0799e4aa31927f1166a93f3b17", + "0x918e7a0f80c4f0a6199b716245e8b5c62a4ef7b5", + "0x925c49deb066e7f23960e6ee08b4a998c0d7a72a", + "0x932a824a4d3d0680e0b352de150e6f212b8ac841", + "0x9e13e43b1648b890d3d958fda96ecbe8708207fd", + "0x9fba5259a1a4654212849e87cfdf5b28d5bbcb5e", + "0x9fea4168523871844e048d27f82c8520cc544a35", + "0xa37116ba530c3c5e784218264b5cb2e7d7150bb5", + "0xaaac0c23b13b881b95b3da37ce3edd5a574acaf2", + "0xab438323e3e3cfdda4b1ad6e9b28221812ce61ff", + "0xac938531f2eb8c7962f88f6a1d286b410f0d4b29", + "0xb08cd8d53c9463935faf555a1ee53e847d98a61a", + "0xb118fd9852ec6762ed38799cb43f0ad1f0d29a2e", + "0xb1e22281e1bc8ab83da1cb138e24acb004b5a4ca", + "0xb39f5c87e06c201a03a8b1bf9641217c31d8de57", + "0xb3bfb32977cfd6200ab9537e3703e501d8381c9b", + "0xb3f21996b59ff2f1cd0fabbdf9bc756e8f93fec9", + "0xb4bd96e23dea3ee89fdca95ce2a1f1dc0d7bcc77", + "0xb5d26158102181dc4ceee75f260a60debd752e45", + "0xb6c252c57c2e275cf6e9229febbbe992e9a81513", + "0xb7e1bff7156576bc7d7af7c7c86101aa02dfae64", + "0xbd9cf606a81da379d1ad3874363746f73295915a", + "0xbe878c39929ed3e79a9fff31a4415da9a45fa73c", + "0xbf445cfaa910f3ddd97cd99da414cb7340c88349", + "0xc02231e00a49e5110034bb5308b02c3471c4d6ff", + "0xc05ed8f3adbc1007d9d8debc21a721aa951fad50", + "0xc1a2d8a1182bc200f3bf9529a89fe9249373d709", + "0xc1fb5319df05f2f814f251428fbf5258b2b00b5f", + "0xc31056abe9b5ebfe82cc7a8c5bd216caaac5a230", + "0xc4270adbc6d6e8a52b5fb8dad0cd196bd7b2bbd6", + "0xc57c4aef79753be888957cc14acf97c72bd53b7f", + "0xca4ad39f872e89ef23eabd5716363fc22513e147", + "0xcea458ef8d2cb23a47443cce3eeb1f20492669c5", + "0xd060f80c6f2d5882bd0b3a83a24d9b355b086063", + "0xd0dc07b98769f23a7bdbef15a35faa256cb65dcf", + "0xd0ffc995157023fe331ca9ade4482ca2542cf671", + "0xd22c7f79be3b5bb06b91abc8ebc7544312045d3c", + "0xd330a1b6713b9e4a2ddc1156b07245eb6694a4d5", + "0xd59586b52f6f8c90c04c2b9718be7b2ac7d6cc61", + "0xd5b806c9253270761adacbc38b198336f8f51e5b", + "0xd703bc629ce9f0ffff0ca09ea2264e0386372aa1", + "0xd7ffafded56a70279d8dc62887c7c16722a22e96", + "0xd8930e773184ff6899d7d88e50f0e98473207f8e", + "0xdadef83811243471424558e8d48b85bfcddaba2f", + "0xdb36e1fa10690f5dcd1a74c2957efc65cf834a1a", + "0xdbf1d832d1e66ce1d8498d578364c0d7d0614c7b", + "0xdbf7241810d4860dae9bfbea488683bfa787944c", + "0xdc7caa98995cceea096aa9149d5f4eb08c57c5ae", + "0xde698282e354705d609c0bc9067174160b3dd5e2", + "0xdfa27d9981ed8f88a8f9310f6d575a3df0051005", + "0xe10996fbf3a3c37ee0fd4a9e7262bc373a6aea67", + "0xe5e83c2347be43eb917715c7385ae2a2cf95453c", + "0xe94560aa36e16c2ce91aa989fd46642b44860397", + "0xec6c2569b55aed5573060f6d46a9202c3471de02", + "0xeda73c9fa6a90cea2bcc5529e9e12c012e13655b", + "0xee4550d71a8ee6db4444099be44ac7100fa72231", + "0xee56a6e8ef45f0d1601779277742ad51a2671d16", + "0xee91d62eb5aaea933efbfd0790613af5db305006", + "0xef8e4277eb5b0d19c13e84eba2adc16e146ab1b2", + "0xf0fa90ae2c995d7b321ba023caac1d9787b349f1", + "0xf4bb7b8cf001338e24f305b033a76c5c3629cbe7", + "0xf72be2c1ae23bfb4045816a0681ee87f0c355ba5", + "0xf74d6bce9d9a670396850a8fdc5bdd67e576bfb8", + "0xf80f396b51491ccec043231a7cd3fdc26b218866", + "0xfa38e7971f4e7fd3d476cbe083eab274941ca8be", + "0x5309a0f7e9f92f535009ba68172b01806d223a43", + "0xc2945ab7579b4c05def2cea0f404268fc75952a2", + "0xda5d647aa9cfa75167d0a9f299fdf01beb9bdd1f", + "0x88fe0288a14499b9a42b10fc031f91eab341a5c6", + "0x1a72992e9058dc444f0df356eb007f6aebc6d867", + "0xa8e156ec480a503410f94383ff6c01b074c10e53", + "0x85ed40dbca94b5bc73c6c7ec7f4ecaad04a03a22", + "0xa019b8ad2cc4b2de16e42cce83aecf8da5ca617f", + "0xdbfc3230dca0b50df050ac5d3d42bf1ecba1b82d", + "0x97ee4ed562c7ed22f4ff7dc3fc4a24b5f0b9627e", + "0x109cbdcf9e4f6cb9ea56c810bc26f06f8b1f399f", + "0x76937f60b76db3b5330b2d257138f994dc160dc0", + "0xb46782ab70619b1bc39e1342e9e77921674e4a33", + "0x14fd7d24ebf81196dfd6d3af740d5024071859f8", + "0xc02ca57ce4fd197bd34877791be417b03bccc049", + "0xad52e66a1ad6efe114cb9278dbf9338e37a517e5", + "0x18593cea6fec4c6351c42f99f4daed0b2751ce41", + "0x001d0bf28a03dbd38a92ca4acc6c4c7008c952d8", + "0x04cd750e3e1d4bfc62c4e4a972ea3580a68a51df", + "0x67b9d361759015d38b43577e0db6047d4737257a", + "0x9c94287160b7fcd729ad6c582440052a949ee96a", + "0x44bd6fd314dd5aae5100f7a9932575207fbc3bd2", + "0x2408e836ebfcf135731df4cf357c10a7b65193bf", + "0x0faa92ab469db5d268fd37c2f1f5e39f72b0e3b3", + "0xacf902451683bbc6f1bd5727a8e7e6b03e5d08e9", + "0x8d7e07b1a346ac29e922ac01fa34cb2029f536b9", + "0xb7b570dc117b4946925bc39166418516fb011cbb", + "0x4242187941a1a2d528ec591cba6bcc4cd4bcbb5f", + "0x1e54deb2d2176f715f719d328ee35f06029236ad", + "0x0815e5b76416dc593c5e82ee7331402d7402a5b2", + "0x92656ad77da2e82731d3cc083116960ce29dfd74", + "0x4815ee939fe2efec2f7bc415f0ce2282f6417fe9", + "0x2b2843a5cbca8ece395665f3c545707ad4b3d155", + "0xf75db6544ccfc7fb5a9df01b9a3eed050562d0d9", + "0xb7360b4425badd4bf91725796a7ed431d8be8e43", + "0xabbf75a59ac8838fa46bd5260501b68ab28b95f6", + "0xa14a711e5d6f38bccf81d703cb8a949f9a1d3cbb", + "0x39bbe14f5fbb3276d06760c95a071ebab6529192", + "0x00dc3a8455daec001e26a5cee271689d11bd98bb", + "0x9763bc90dd4e1544da011833ad1dd25b8c0eea96", + "0x528935b9889780e8ffe3b3abfb614d31718d9965", + "0xf9026c6ffb136cfb190b95bf617567a23452d823", + "0x6ed88188da646b6fe64022895de4045c35eba48e", + "0xf6831d052c9dc1523c33e7bb9b657a33ab84d5d2", + "0xb9095b98bbd01a93c395cb2e48e4cc256d05ef8b", + "0x7caa741d741a38be582fa619ea51f4657d85e85f", + "0x12e610ae4c0871444c0539fc7563c5d3317c615c", + "0x83fe7223cc6dc1430dfed5435d4a264f2b799100", + "0x73ccad884e29ef092a7e8b3e943b91fb3881a7d9", + "0x318c012875477b1fa77bf1646df59b90272a0e26", + "0xac47e58de553bb5ee2f30fdb5ff8e6edebeba90d", + "0xcb966829e34f1b376715471398ba3e1fa50681c2", + "0x7a44834d4dec3f21a341de51365fdefdd41faa98", + "0x488a6a10ff29b278f03575d466a1f9d6bf494ba3", + "0x4d4f26bff1022eacdc8369aa12bf800211be86ac", + "0xf0fcb5b2ec2d511c4338f59528baafdaa1f3ff37", + "0xd13cf36b646adcad473523f7b32baa74f4f8f502", + "0x592e1e16016c25651513591b7ebed5ca803ad39e", + "0x19dfdc194bb5cf599af78b1967dbb3783c590720", + "0xa529dbf959fc80f9db66d990b47eb33e26c416a0", + "0xc9f7113042615ce4796f8cbbfa6f42170d908e05", + "0x27667ac10ca637448ed6cdae46ebb8347bfa6b34", + "0xf6ccf8751fd02de2e14cdf0ff0aa36d6e7a1ab28", + "0xc3a03c3f58c674fc41756744bf2210c8bdbc4081", + "0xc283e7977583b2d17353bd17d01d953a770a5776", + "0x906b4a9f69c1bbc14edc8d141b4a22df6726592b", + "0x79f38d7d21e8a3d2e91abc2cc2ea6747dd5153f9", + "0x57c6bdebe4702beb4ab070c903edfbea575e0923", + "0xa25e3a5f8268538c3726ccec9624056973b71d2f", + "0x9be8d78f7733681189adcaa5634a2dcb53fe29ae", + "0x1c00268fa3499658124b8fe6789d371c0a22a21f", + "0x798ed654f4d9599b99f94f4a61609b7a7cfbd200", + "0xf1bfc5760c1aeb6bdb16252142429107d4ae0997", + "0x7511b3d9c01da239e4f0c3d48e190949f68e2b03", + "0x48e87c81a1aece2a39b4facd8efdc3fcd63c636c", + "0xf5edfcde8b5e43ecfe382fd5855612b3bf611224", + "0xeb4576fe753dab07635c0bb6c8f0a355e1db5d31", + "0x408a64365229c6097dab7f23c8e2bbc906951567", + "0xade38ad7cd00c20cdf60afdd679be32f87b7d802", + "0xc079acf6caddaa3ab905c5fb54330c937d19f60b", + "0xd2e538812961e563b82a13662a8c22841c1e459e", + "0xe2d6aa050c655a3f0f95efd8f2107778090308ae", + "0x2e171682d2e7962fd7518dc2603f413cdf225f5d", + "0xbbae0e0795294bb37f9b7cee715e7f15602f67dd", + "0x9ad08e2742696174e1277f7710568a2724efa82d", + "0x1d0b6015c2ade8ff59da0e2e23d0655fd8b3b3b6", + "0x747276019e3340104c96397bf6537ad01f93d7df", + "0x897b5561bb4e815b486952391fd2e27c648f6d32", + "0xb119efdabf71e3b38cdfa393af50e972a1149e89", + "0x0d8c573e3938713c02ccc77d569eb0e5bc4bb20d", + "0x3722d1a53fba65dc3c2abad6c5df0f1c969699d2", + "0x44e8a67e451cafa616ae785940e9c4cd574ae3b4", + "0x73dd0f5b465b57570156f25907fac0f38b05ad0b", + "0x3e763998e3c70b15347d68dc93a9ca021385675d", + "0x00ab97c782c661fb3d08c96dce06176938101211", + "0x0e90caa1ddf2184bfde42cb7daee3642f9ff54e4", + "0xd8e51fc9a41c19bca7646dc054f03c40bc5ec6a8", + "0xd79c597a5d5e4b093c4de8dca8b9c7927d0d4b68", + "0x0de4348c28c4e9ef19d5faf4024ef6653c326ec8", + "0xacc43b5c8e2ed12d374e74cd91bc576fbcb9ee23", + "0x5275cc5a4bb7fa74f602f7ed90b1bc3579674ff9", + "0x0d3409eed02927d6298ab98bc79496cbddecf0b7", + "0x8d67d01acd6fc40420f512c044baea0f0da0056f", + "0xeef0b8c8388a332973dfa7a110eef1f0de8670f0", + "0x8842f97d36913c09d640eb0e187260429e87d78a", + "0xce3e142bc70f5a0f674a2ef5649b3248842cf1a5", + "0x25e668e561f617845f044267ad0dbbdc9aa36c6c", + "0xaaee61b58dc32e7331104f7a51d2a3220edf4261", + "0xd3d5458c07b655cd79d4814a52f42eb8fc59c24c", + "0x93e961b3a375c1831db9c3ce604d4b54f731b4d4", + "0x37dd83ebde2d144bf68be9a6686a3c9bda6ff73d", + "0x31de2673575c1b0973e628b8bc62ebd0f4bd886c", + "0xed84906a819e3fb51c0b62bb7d7f86262888bff1", + "0xedf5025c84231784293970a5b8a1ac9d29f9b758", + "0x952cf57d0518bc03019d397e06a5a9dfcfde2caa", + "0xa9e839484195fd7685374f29d67f68590c8b2bdc", + "0xc3a4fb7ee898f675bd691c05f43ad5a67aefa81c", + "0xeb32515ed999140479893d96b5803e1c0b014bcb", + "0x73422a2722066c1750cb9fcbf79cabf1f08f9bf4", + "0x7822159ee394d14745cde63a706f965fb73c7ac8", + "0xc2171f490c9ec7d5c6c75cd6d9d43c89cefc6867", + "0x5458210bf50ed1864639f45b5ae7df76be2177e4", + "0x609470c2f08ff626078ba64ceb905d73b155089d", + "0xe0ba6c4c82376c76386d1ca2ae2516ab18882e3c", + "0x9188714419b81c4c920f04876c19ffae8f5fcc26", + "0xc29dac18bf481c1615caebea293b84c15b383828", + "0x0d268e9b0dc5870fa1188ba99fed52edcc81a985", + "0xec4a6f59960fb55a7fa49262e2628687b322cf62", + "0x60ae40235c11116fb74a1ecdd8a175b4f955a1d5", + "0x8074ed7aea09a7f371af44dabd4de6c42839929b", + "0x2445db49ce5e58d97a2cb1acdb8af326aa592f8a", + "0x30f89f5f4cf25f457b4d9cbfb64a64a97f00bed0", + "0x9f9692fff5ce2c81737f62bcca101a7a7bc31c46", + "0xe601043c33d78eaebc8e27e0436e7a0d11efe1b0", + "0x126ed947de7b84ab29526d35cef99c9b72b285a5", + "0x2f5a662614b84f37f71f25e4f56b7562a0e71786", + "0xa72e1756426100c6207421471449e2ba9a917e86", + "0xbb4d109c3dfe45ff30703bd2de43dba1c74caee6", + "0x64d196e9cc62ec70ca0379dfc38ccbff344dd38b", + "0xa82d4d63fc6332adfb593cf21a649947f8d5464b", + "0xe93f324c0dd0f9c558b9a4652927e5d06cbe85b3", + "0xcc112a107abe98901b9efd9ac4880a143df48083", + "0x64368face21029a646d823c0afcbb4da08ce9b78", + "0x5882aa5d97391af0889dd4d16c3194e96a7abe00", + "0x702c0ca3d5f5f057671074466311c5d780413932", + "0xa4f967b84ff09e0bd4368ef934499a6a3e4bee22", + "0xa756b300e0b546dbad3936d4b8fc9bfbdfe222e7", + "0x7287c3d93b89b7f9153fee6ef086cd2858e9b9eb", + "0xf786b0717f045595c5a3904262b8ef649bd666f2", + "0xbf88d8258202f26e92acf7f0349c12c6acaf4199", + "0xe2d7c679aedc71dcfd65eb381107f8beb0f65666", + "0xc2dc013edd48afdcd7b872d51d55ccd1a7717e28", + "0xe9e2c06423d41e91e63de4ab4a68b3b4db0e681a", + "0x615edf07705ed6b2d5ac012f780236df898aea32", + "0x6396a2ace5978c4fc9ba7ac3d2d3efb6b3055461", + "0xe13a47718b9bfd9f74008068787827af3e5931b5", + "0xd522d275f7b178dcb949f96e5dcd73f6995760e1", + "0xf6c25db73423acb7249e1fcc4ea5e3f7f1eab4df", + "0x1f05dcdb58a47a33d7bce998fed28e2618b4f067", + "0x56fed5bb34d8ecae854a58502befe1e1fed679c0", + "0xbbfbff2fea1950d404742deb24ece126f3796506", + "0xf1150f15e5dc578ee6a960569be240db7a9efdca", + "0x3187a54a100e7dfa4fa60c5977d1921223619f2a", + "0xc4bc1d401b997c7855f4de64d197b41cd2860aac", + "0x610bb423d080dbbb9ea5fb8bab100aad2d81bca4", + "0x94382cf5d7f6eb2de5e5f41553086f08b14e11be", + "0x71535aae1b6c0c51db317b54d5eee72d1ab843c1", + "0x8e900d7cc784a47b2f5b4baf0871aadf792d0cc1", + "0x352bf5e59a4c1a0e4403278fe5c9189f63afde3c", + "0x083e952464b4a8e988abe39868a652f9c2ad050e", + "0x0a83985e4a6e8dae2b67bed4f2d9268f6806ce00", + "0x5bb872d51ba73847dcfc5031fee0af568d2d83e3", + "0xfaa41900848b7f68f4859676432811e6fc46a15c", + "0xe9f6b6bb8ca582e0e5dcbb03e95b247843fdcf2f", + "0x7aaf6b8506f030f0dac415d03c87e89dc891eeed", + "0xcdc0b6ea0875e8d7519edf7942625111685d6e9b", + "0xfa122d9ee820873027411ffbda06853439b8fef9", + "0xa3c6312795199efe026912cdff4b4e391a7f17fb", + "0x12f20fb5b6047f129562c97b58fb2a78cd80b765", + "0x68c3cb93a893a893953d654faa9d2dc15edccdcf", + "0xa3b926f6d2bb5507fe711847640ba2086cb11a75", + "0x73961ce0ac9220fbf71c86bd38dae888f75bb169", + "0x9a483103327b3b0814abcbf61c4860c48fd84f07", + "0x14da174fab1356466af2dd0ecdf94204e828a732", + "0x4355ea5f88d29edb19c8d5861b11cab5c0f7cdb5", + "0x3352cb989e5cf23335af7f556e15b824f7e2463e", + "0xb5e0c5a26ed9cd9c1684fbb1138d5952c53349f8", + "0x092121fd7700b97e68248f8763c4f3cdd955d5a0", + "0x0f2f90aa248c8960db7a5e13a52f3769bc4accca", + "0xb5d64a8720e8dd600aa30965cfa14799df6824f5", + "0xc2c0331897b98c72b092625c473867359ec5f6cc", + "0xa0e730ae29d4ddbc39c40f94d799b75c3e17bd58", + "0x30a26c2837e9ad41ea5955949f00402dbf86f124", + "0x8b78eb724a22b422891d048012606b5563e62924", + "0x3692854ae1ce9a91aa0db6888105b466c767e112", + "0x2cf638a9465b34408847c69aea170f2d2f5e3153", + "0x0c13f7c69f1b3d05c0a007e29f03b4d948b0859d", + "0x21f701ae5ada0d8a702947460488a1fb43a3ee60", + "0x2637652ac58addfb67b401a1befb5f48b967574d", + "0x3e5944908451231f22092218129f4b7d525e2e61", + "0x99a5aae28eac0bba6e1e2f6c1c0e5a87612df6f3", + "0x291f1593c2ba68974bc6e0ae715b52ee313813a6", + "0x8922923059388a4488fa3b7c418bd059723bd37f", + "0x4e53433495619613251ed5d9360d205a3b87d047", + "0xa40f36d4502a05d215cdcbab5693d10e271a587b", + "0x448749280160d2126053330fa7a18f893943c455", + "0x35e45b6421c01a131fa42a00f34091c3007a952f", + "0xbae6379acaee0fd0d5922caba1895292533b6fb9", + "0x14b8bffef5d546727df9a4357e69c49907656179", + "0x9cac725f17d5deb4a65922a6043eac4081ac6bc4", + "0xf848c171f708c45b28ab299cd58bd239b9836939", + "0x44a67412f3b7298db589e456253172a4fd2ee541", + "0x3ba827c00bbca8d1a6a81b0a590ce9c82c0ae317", + "0xdfb0348f0d8200ee2eb50aa4755f690dc3165b52", + "0x023ab8e20a4682d315daef4c91db96bd77934d66", + "0xacfb5d441f92339218ce5b34a794278001de6073", + "0x93d87d3488b2774dba828a53af99b083587bcedf", + "0x1f6e332e2a1bb6de6104ad18408239762770ccdf", + "0x933258bddd49beeca77f6d1889633c5429af45ea", + "0x8577f44ab55c0b1167031ed26c20a4dbe26868dd", + "0x47c869583f24e7c187f4d57cf1c2736da51f145b", + "0x4bf4421ce8208b2e54c0b7e0b142192b47e69c4a", + "0x5f4d787692b0ab325df37e0b999c452d4561a8e6", + "0x39b2bcbb9fa29d72755ac2446c310fe98c065a12", + "0xa7dbee7f9ccdd8c91b75067cd31cf0b6d594fbfb", + "0x42fe01565e60d0687ab793de8cafc1e8a39816a8", + "0x8af7810012012ff02f0734d46d09ec1dd058cae8", + "0xa973d0968d6bbe4859bad50379d87c91be615b9c", + "0xbaf88b32025aeaefee010013da46098382475ddc", + "0x7495f82f53f151decf4fe96eabae4f395ae3e2c8", + "0x5d77392dd7ac01af37639ea303fa44e980c83f79", + "0x0ff6e1e68413d2407fa6161ac4db8f21f4d3ad41", + "0x304b3d778ada6d14387dba400d48ec5bb9eb689b", + "0xe96664029ad91b2d9928bbcab585450f78eff8cc", + "0x1cc3a9b765199bb1bea3cba45b4a203c55ec4e1a", + "0xadffc760edf4f6146dce89022b5ae7ebb7edd2b9", + "0x4a795509a521d22123a66317708698e81d147714", + "0xb8cc0ae73c5ee88ec33bde0e3795f0667bd46d19", + "0xf925cc2cbea0b2b30aef6c29ec0c77d7cb6fcb6f", + "0xa4264aa379ad275afad55b77d22de421bf425280", + "0xd27bfa0bb69bd04cb869660b2ef97acf0ee3a707", + "0xd1779622b76824f952bf89b3bc009f6db19be701", + "0x3ea4ea76efbc4da7207635b3705654ba8d674a62", + "0x1012e83da77e283910d00e9dc1af5b103d62db12", + "0x560d03f2b20e9047714fea87cc113d95a3fc7179", + "0x14ed950929c02124ee464e930070614a65316fdb", + "0xe51f6b8b148bee49c8a7ba87ce088d7081ef6912", + "0x86ae657b0b38429e8072523f459e572f777ca276", + "0x7d1b10577da49d490685f8105bee3fa7a82ced53", + "0x8a1e24491a2b76e94f69bd9f980d0fbb8c3b75bf", + "0x4b1f140a82cf40a955d1e8ca2aa52e128b593722", + "0x31345e8a872c5a34e6264b1c2ad3fc8d13a0734e", + "0x9cd75894548a8969e924f0b3af6bf8dc27612b71", + "0xe139040aeb075c3f0bed81bb66f567b5af19cae1", + "0x6d7fea7c2a257048032290a8d4a4b3f966f67c1c", + "0x8eb9da111684294f6d57c8c400c71d33e1221800", + "0x3f5e80115268633e19d97d61a2752e1271409e01", + "0x5761774922b95084454c8720d647250bad207ea2", + "0x24046ec223b7a42eabb0527d60e0138922bb933d", + "0x03e236ad091a944b04e0311d329161c2a15d6012", + "0xe1ce761aca1cf320d3185b6eb37d58a81fcdae2a", + "0xcdc95322054561dde9b80c2e5a22f8e09bcb80e7", + "0x89d1a663b53d5d7831f67719f2fe4932f8251f9c", + "0x0442f45807cf2fe3a3daed99762cdd9ceff853b8", + "0x1be8f34da50d68a5452538ce6c53b43daec148be", + "0x9c9553cd0c8673651a36e5e11c4989bb629d4067", + "0x821be125973279e5ce5eacf2f9360dcaa9cad53e", + "0x7ece13f2fee7f2ff9cfb56b1f6d1ef2a787d68d7", + "0xeb057c509cf30cc45b0f52c8e507ac3cf8e78777", + "0x1338fef4210fc6095226fb6388077c66f153087c", + "0xa9433fe051aee57b1de0283504be74b1d6cd39ab", + "0xa659acbd4cc99a321d44740182b6a8b9264b0167", + "0xbdec7287c08c0c153f9843c4796d59e3bf7178e4", + "0xe9a4ab491942160cc1f42449911d0e3dd5222436", + "0xe57d3adcd6818b8e40bfcf4ead289afaf4d40b5b", + "0x069e85d4f1010dd961897dc8c095fbb5ff297434", + "0x2b01501c2e2e3200e68f1fdc557a10c82bf05626", + "0xdc46bb979bf5102d78ece8e9a91dbaec932e756a", + "0x3b1fc9653f03789bc399fc343d8d5b8fc1520ea0", + "0x1e2cc37406f868b3d481170248e8ab2fb0181644", + "0x7a7528c26cadf1c4cb3014f1a99f5f3204a77333", + "0x4cdd6ca7da844aaa5be3c7bd5b9cbd213c77f6c3", + "0x6828459e3acfbf6b787c4ca752d242e6340d3018", + "0x18e8b8fb3c843121b24e4fe7672fbc46c3c50631", + "0xed933b393bcd6f138b3843f5bcd50e3ab6e673f8", + "0xce1cb8bb004d6f3b9f8166e3fb90617c5b723a70", + "0xc067e7f26f6e75bb6591acf1fc50b6a5dccf5835", + "0xbcbfd32b980e95d765c49728960da9f4504947d7", + "0x9633bac0ae8d720e2016d8268a9030a6acadd784", + "0x7e9d2fa27745a7a61b5b433d6e413496b2da9ee2", + "0xcaf76ca5cf79510f7928eee79efeb92372957c10", + "0xda90a6e5b488a617c49a412a198b8187b7e9c999", + "0x5942a257bd448670ae4353b06bacdabab5fbf0c8", + "0xa9eaef87c01b4c46a691862c7ba94401394b8b9c", + "0x009994eaf678d11b17151455cae0c87e0b2b6825", + "0xd6ab3210442765d0bd806db6643da28d307eb422", + "0x40bb0be4171a888da2a6c46ac6ae505a63fb3ae9", + "0xe0ad5f6422687bf667d09590383fdc3c0f8af392", + "0xc57684e4bfd9fd5227dc1c66ba03cb9592f4e492", + "0xefcb1b77e1f14f6b8282535b96ce61b6ea99f1a8", + "0x28da3dde285d8f1f87b2d858f89961bb8b9af180", + "0x6c2693f5a936f37ed03cfa8465bf2d8beff19a0f", + "0xb9806d3a651eca707dcee20e9e82310754878fc4", + "0x2aa6b66ce9432a67953d1f37533cf0b05a5f6e08", + "0x2b05cca9ce08053b8aa1c3fdc276d41a82c0ad36", + "0x5430ffc48ff3a0d4e53574bf89a7e9f75eba524d", + "0x0952222d50584a63fbe38d5b4da94b06db102d46", + "0x623331fd48abca0af5457ba39b8328979c7fabd0", + "0xbca4d68be543dcefb1a8bccb519503f9ba3f2026", + "0x3b6413f38da132c1ee3e1bfe603925677e5ceab8", + "0xf51e5a0a85d29aa0508894405d734bcf044dcb5b", + "0xbffec9db5d96f5f8367c3c8e95c25eb8b7d44ad6", + "0x021f1c3677c19b13093eb39c445a4847500260e7", + "0xbd2eab5074658c0f3ff0ef7045563cafda008bcb", + "0x98ed1eae1836fa93a20fa389fc386ba75bd07d5b", + "0x69c509e9765c49cc6b9b4568b90aa47b89f4992f", + "0x961ae23f0cd4cd82fc129563ae23d81223aaa2d3", + "0x0428fdc1f59747b811eb3834344e065c216241c8", + "0x7be928201ca49a2337e3951a1372a6bb26a2d058", + "0xfc19eddc148e92a59c67d1ded4bd7f27afea0194", + "0x8c1f48a0db29aeab0e0ca7214c92cf6cce877279", + "0x8da3280eba3bfde4bf096b4e7b3ea45ef70aa3a2", + "0xf712ab8c4505eb4b36a2ff9fa746470be81e6538", + "0x3b46a777dd4657d91ecc9ee3efea1162ad1cd9f1", + "0x8ee25bb8d6777c21ceffdea20583856c620dcae1", + "0xb44647814db8ada1b67e8240938d2a6bf77184ba", + "0xa6cd97fb84c669b636b86e0c2cf5e3428363a6f6", + "0x867c55acd5a4691d9336b26bb15c4dbede5c188d", + "0xaee5cb72da840cf679ebec780e31295b6bfc746d", + "0x4564d6d107a19d3ab3d734a7bae61eb63dc3d30f", + "0x8c11e3af9c1d8718c40c51d4ff0958afcf77fd71", + "0xc9c9077be44b06ff3ce957d72149c65128f14331", + "0x0bda0f33311e65379461d0a555bbd7669c0eaa22", + "0x1c6e47d1eff818156781d8c0155153ccff2e1aed", + "0x35e8171bb85a471c8e6b379c19515006dabff236", + "0xb329f33d249662b0f2980f823435fdc7aec3efda", + "0xf28570694a6c9cd0494955966ae75af61abf5a07", + "0x56e08babb8bf928bd8571d2a2a78235ae57ae5bd", + "0x40908ee8c5739ad1ec1031b480cd88372bbc5c84", + "0x5d139ac18df75fbebe6d21dccf47dc54cad74f7f", + "0xe3e7620168df930e62fb8fd04d6e8049892eae1d", + "0x2a724be1b63caec1c3ef95834fdd443c3347ee1d", + "0xd18b8d61c160e779cefeb61d47600061007a6130", + "0xb40c28f91aa98a2234ad9e5693fee052cc173784", + "0x8f2b60bb82e11fa9539bbf88803ec079d25f1b86", + "0xcfe5d190bd42fca25c675cd0910caf1e985baf93", + "0x12fdd8f4edd6df194b0b974f21b1195ceee074fd", + "0x3fe7f445566a103eb5d62c7890387f717fc06e83", + "0x3b2dadfab8fc9859d2aaa9ad09cafa2b179f67bc", + "0xec8804867a84cfb397debad4e1bffb1f015bd7e1", + "0xf291280602a6dbbdbb08d48557c41ab4c45f8b49", + "0x885aa2a20e2e854a5cbd8ada548b7f4a8ffaa0c0", + "0x190ac830b737acfbfa819f1d3ab0a6d702d72147", + "0x908483225afd362febe58f032e1ccb82002401bc", + "0xebecc0a1eb0a05f3ce7100f20ec6b97cb93f0965", + "0xec57915526a7869b9b5539b31c90bc81a060a171", + "0x24617cf7babb16681184f9335834a7f053f42612", + "0xfe6e29c244dc1d06c26033ffd056e0f81224b64a", + "0x0eca5bd7b78603c3a4b3292914c98b94500f843a", + "0x1874281e2386d85617eab117259f2157334243db", + "0x3a92b4bf4f6528dd12f8b0feb4e91768f65be781", + "0xf28dce94a86f64606bb80b38182c2962157bb2ca", + "0xb9d1a4528ece05d7451a1e8f42f60c93a390e350", + "0x4cac5d5403996df53c3b2ff5058415665db2ff18", + "0x0d7dc488f8276ac1e651d982ae6a16dff089d813", + "0xb5d47d2e05401c21303903fda94285a05058808e", + "0xa582da012a45fc26a61a134a720abc913938eed8", + "0x0fa88472c52491de94d0d40c4ecbf69b63c6e300", + "0xba208f8ba2fa377dfa9bae58a561d503c3f4d96c", + "0xc1a0bc536251215fafc9ccb610752d8453545138", + "0x5b3067c5e26fec823d6fbf8f315e43632658d2f6", + "0xff7d94d50aedb1ecba19a56f5a1eed9371071eaa", + "0x9d99e6102562f3465988402831b3843a923f3aa6", + "0xacd327490eeb54cc7d1c9c28e3b5a39f0e472be3", + "0x5e7a0afc7e3c32f844114d3cb375ab0c456a57ae", + "0x9e96a27867464c9f8a48935b143cae922da949d6", + "0x05a2e50c5e4d724897b67b708db432a38c985f83", + "0x9a499155e6d5f6a8722876a5ad0c4a47b3322c87", + "0x6803bc6e4bd209537eb356feb03c66ba4a383f1c", + "0x7da220633dfaa2dd4ef3e3b222e836a2e23a496b", + "0x271742440a1f6bd36de80b8f7900d1ed7dadf3e3", + "0xfb15339d187a76af62698f73d700e1f8536f03bd", + "0x8ebd118444d8239505d66c8cb6ee36eee6abed5e", + "0x2fef6742d30c81c518d7742d5c7ae6723f64a79c", + "0x493409704ec5e2ed28fdad4f48638532228ac693", + "0xb616d066fb1ab1384d47a5a70ce00af515445a6d", + "0x1813c5eb6698250fcd0a4bebd06b9ed8eaff275d", + "0xea09cfc7239476ab030b649d5cefdb786f0e7412", + "0xa762e2e666e70da57fb27d48dcf16b64535cb170", + "0xe51d044586e4f3377c099aa67bc523b3b3cd5119", + "0x7d0a940d81ce63ce716a1e3cd7372f78f79d2414", + "0x083df0282ab7680098af6059f0422ef87900d22d", + "0xee700cfa2324cd9c06be498cc65de24b6c3d2893", + "0xa3accb1facb57a66720b1873466855337b6d5a20", + "0x10890742a1a20a936132072c20ae77b081486190", + "0x63c913ae5e66825b21d83aac5b58e859ef2abbcb", + "0xdd30dad7a3efac0c849e5c949dc1fe6b233a3fa2", + "0xce71d1b2443c8a7207965d54db0f74b29261fa00", + "0x225948313d4cc7b5953ad698740ec333df52a8f4", + "0xf6f8bb3bf0b250d4b41de6bc20fe57df62c045e3", + "0x514c4ba193c698100ddc998f17f24bdf59c7b6fb", + "0x8bd7af715cdb9f5d2abd5da5138671eea19eac70", + "0x3523481702186c04740e50396d0e1c807ce36c57", + "0x1a5cb0c69c2e607ccb6dd7fe7bcf394636a4215f", + "0x094791faaad2a4a10f27838bc354c4d7d0bd8e75", + "0xe32b9ed080d3051ab4f291d9f65d7528d139ffb4", + "0x06ad2f93b61eb355e6e27d9eb8c67ef8eccfa50e", + "0x3374cb587bf1bb914946b692ef7ae80f8c344091", + "0xadd2975595fd251587844d9e0fbc8a31341d2a27", + "0xfb5c2bd48e34fc530eb857acd71905e2b17b1f9d", + "0x54bdb287edd2b9fba7cbba098783bd14b19db93d", + "0xad24d3708240240d3f141580522b13f01e15aa9f", + "0x439ec0fe84620c097501d3eb6079b37204e446c9", + "0x03f65d114dd493b129eab94b8ed8190a464d3d5e", + "0x9979375a29d542a16d1d7042b143c9eb07cef0ef", + "0xfa9413b9f08252588b4040f888c5f758ed6d10a7", + "0xd9af06ca2eb971f818f0c41d40e5b8f89830d7cc", + "0xbec85812e620b56525681312b12efce711a58135", + "0x3f7d5f190dbd3d0f68fb434efdb3267c02e6720f", + "0xa5504bcb6cc0492344151ebee943c62d2b2c071f", + "0x989593a779906a51596e84665e24fe2ef4b1f3b0", + "0x2eaa6ac9dc11b1c4700443f52fbc9d067c1d0d65", + "0x4f8efb4cc83a83e908029db0a4c782b651a3bb83", + "0x7f4437dd2881468525343b86ef4598d24a21bcac", + "0xcc83df78030cbc991df847f353b189fdec97d359", + "0x4b2651d278966402a6dc3f20f60b590dd93a81e5", + "0xdbc35cce779e29fb71799137f632b3f6f7f46573", + "0x65b064726fffff3b3667718d2264e84367be5141", + "0x3491360d98559ec4146fec5b9886d7f9f3762503", + "0x32a1874657673c8403059cb30880612af7d2c9c6", + "0xe126d84a1c2018b5f5ff821c1539eb8c53f27bf2", + "0xa872a8b67fa8047265ac4bd38022107b7c30f85d", + "0x61d7ece32854caf46c6304fd536704eacd4da256", + "0x1a6e210dcdef318ca8c11aaaab7e305e49eb0e7d", + "0x07e6ae8f553dc77b8b372e4d20dab797475e6119", + "0x98b5532ff6201d9bb679eee90cc34c10137998f4", + "0xe78705fa043687cbd7b907d91730d1fe5ce66365", + "0x95db2fef9f52ba3f8cf4548374b9135dee83e8c5", + "0xcc9799c6933425d68687bb15c817af0a667fad87", + "0x804c3083d6565426a35482611a514948abfb00ac", + "0xab99c070540ce0aa4c7b978584a6edfca5af37ce", + "0xa55b524dbdd2d7080faadae7807cba904ef8a827", + "0x11ee7dccc1cf223318efe308ca55c282f1d355ad", + "0x709cb5d672cabab9d50250c0af982c52b1b233a1", + "0xdee7e15ff122c733c1f5e8800187815391c8bbc7", + "0x84fa205abfc260106765e5daabb1346db354ea74", + "0x69ce31018c82ca3d2d9e4c5a6d83161b4320f9e9", + "0x51256ef00c2485f3a40fe1b6e4e779147bcdbb58", + "0x97992871d78116a928add496850ec410a31fd564", + "0xd0fe971732e547059ea6009c79a2d6155bdb41a4", + "0x1125917201ed36700f86c3cecec8c5dafae280d1", + "0x00e8ecfd62a04ccbf2b22e7fbb6992900151ff77", + "0x9ac63a553219198ada836731b2acc1596987b417", + "0x5a052a9928d100bb68e14ed2a8091c1a758e1bf0", + "0xd35e24699cb637207c7486a570039d30fafa824c", + "0xab9b83a7f3416d921464ab018ae273fb4b284e86", + "0x1393d846210262af69a29587aed52c24b0ae6223", + "0xd7620731c75b0f4078707be8f1fd6b6b7b277a3e", + "0x03f22215ebbd72fbb98b15d39fcdbbe6bc7b9a8a", + "0x84de7a37f1d1c05aaa1502f87e4330adfc9cd32b", + "0x8fabcf470152e44e01750d50f3631f538b8c5d8a", + "0xa28ac1264ea4dc2fcae85c379a25e24b44a1aae0", + "0xfc2098b31f7cd6e520836b699cdb98cb256beef4", + "0x5d824ac4b69776f5f3c5961043d684ff6091faa4", + "0x716410bfb864a494df9b943a63e942e6e895d21c", + "0x69da58fff827b1cf91677b72b8fc515f61784762", + "0xdaadaf4f2d94cdd4274eecf794c3d5c27f647476", + "0xcffc127c4f491856cc93433cc5c61a18a6def6ad", + "0xa2c12dd46eb80ad3ebfb9f07b0293b2e076bfc5b", + "0xca9c7e1c9bb058d92f1ace6a389857eebe7b1368", + "0x5fe3971e91a40c7db540be68d0af7e154f1d4f54", + "0x1cc6722c1ca27b1b2d3d89ee5854b4f76fce829a", + "0x62bc9ba8770f43011d3339ef30f50c30b404edcb", + "0xeac4d6f1858e6ad8e99c37fc390a4cbb4d519d62", + "0x3d6991085ab1ae3926cb96f25684c40a364b6856", + "0x49fc60bc9c4420c8af344966e50b6f1b4786910a", + "0x765e9af7210c2d120096ce5b5709e8060b710474", + "0x9ee6f42531adac0bd443756f0120c6aeed354115", + "0x65877be34c0c3c3a317d97028fd91bd261410026", + "0x380feff90198bc240e1c47d4e3c0b51391dd2d63", + "0x423d1542697d3b737151c1e7ffec5c82dd5b4b12", + "0x1406a69a6abd0a3d904876724b20454b3eca5eed", + "0xf6f40ced4cb39ed9eb2c2857b8cd0ccc2ddac241", + "0x42a82de905239e0c5e584e0c80d7a15825b9c190", + "0x18a428ce0c31584a21cf8cefdb8849d8013e1994", + "0xf9df78d070f24c644d35a56a8f2971b590084e9b", + "0x6187053cb6ee01e5d834a0a039da91060cac1cb0", + "0x8131f8bc771733d44500cc35464fb9d451932269", + "0xd80da4487c1ad77189821dc50696b9a619ee39a5", + "0xc0a674041c3ccb2dd8e056567bcd6a14314b63c4", + "0x333287ac13b0bb9251464daffe18dc309aae770d", + "0x81db2dfd33b24419f0f715e331f678411eb1ec30", + "0x7c11a14d2010e3a0a93b6291449cce10b2e38f66", + "0xa63ec0012bc7334ba5cfc22901748bddd854bee7", + "0xac631f4f75b5799f9149e70aac3af88658487f84", + "0x9ad92f7f5ef6f4bff83c2e10298a927f0ad7952a", + "0x0495c779401b2d9f8c56d9d156f4b4b653ffa2c1", + "0xe2efa7c8a0769e91af174870dbddc2d2500c08a3", + "0x7ffdd3aedbe63fd45570e81f7f75fbe99463470c", + "0x5e42cf1d478f52458885f75df041c6246ab272de", + "0xd583f58e120600961e3e44def13718fcc1c1f491", + "0x4accc4f0781594deb1c382ea8a04b566927cfbed", + "0xebc8c2e0b8c4040e18eabab8459621faf1f6513c", + "0x3a51c9cfc601e6192d24a33dfff394ee8daaa144", + "0x0fdf1ec1f12dbbf56a8b066985b1349cbfec262c", + "0x2b131ff66b1ca0348e1c20cdb1e5069485405f59", + "0xf93d4ec4cadec63a68fe13ad5bb1abed44ebb81c", + "0x9acf8d0315094d33aa6875b673eb126483c3a2c0", + "0x4534b2b160217849776b1885e082b0e39ec7df08", + "0xe4d97ea3fc1459a8b2643a7cb07f7a4471f32f8f", + "0xa38f13b62142230f01842f63a014aa59652ec1a1", + "0xddf169bf228e6d6e701180e2e6f290739663a784", + "0x0423275d2ac0a164b91cf8f14bce45fd6898f563", + "0xead67d6463cc332507c4b321b022e84e5fbcac97", + "0x197b7dfee0edbda94d8e718378dfe569e9c22d42", + "0xf38a9b66f90a34ae03e21d75f398e60aa895e45b", + "0x6cf51fdef74d02296017a1129086ee9c3477dc01", + "0x7497b333895c64a56924b293e68a6468963f2a7e", + "0x4291ab8a26efdf45a89f1b4ba2aad8229a778a9b", + "0x37df7f9254a44ae8acef12f9133724bb78f5e5af", + "0x08706c91ee7bf8016f8b33f3530fac5478120fbd", + "0x0055d4369a59bc819f58a76ecc3709407204dbab", + "0x9c4b88fbe06b6b1e461d9d294ecb7fb0c841db4f", + "0x77eeaf07c050a690f9b3c2e8e7642cc3cbeceeb4", + "0x01ebce016681d076667bdb823ebe1f76830da6fa", + "0x818854b363b90791a9ebc29e2f9c7f1055ee5a4d", + "0x0b99363648efea66689d58a553bb015957083c57", + "0x9792e2e8f5a07be11b0df35b3bff61932bbf8456", + "0x14ce0d3074e93f2761810fc64183a2ba9fc33481", + "0x5ab8e8a5ac6ee75bdf3ae1e9bb5e354232768053", + "0x6afbabd050ee883a2e8125d367e5f7f410172e57", + "0xfe407097e296c521c2beaa5e4e61a9b91bbe0d61", + "0x534b86280545b270b0b0fc2a57096c42290b801f", + "0x679bcdae56edf5c847e769ca9eb82ab58c93351a", + "0x1f87ee1e1e73fcf1daede163b3f40eeed9db7ebe", + "0x9aef7c447f6bc8d010b22aff52d5b67785ed942c", + "0x393a256ddcb830a837fe821558e342d096a5f54d", + "0x9ebc0b1bccb2c2412b661510f2b2e67828347f0c", + "0x3b1b211b3fce0d1ee9f5e1afeecadeb98378d0b0", + "0x939714d0fecf95d77d068838910cb6ce921a08a5", + "0x395ef2d7a5d499b62ac479064b7eaa51ae823a22", + "0x4bdfae7d2a6fc206c5a80f4d41e683a5d5b30d5b", + "0x9c1ee1dddbef7be7c96df3be5c63998f18f4c9cb", + "0x8c4ec20aa10b617ea6c008bf7a94ec97cd643bbb", + "0x96a6f29ca861b7420c586e070fbc767e357ba821", + "0xe66b290105f73dd5f38b25c7d4c86d2c5e5a37be", + "0x02a84163763dd791328765b96aed43b6a915af48", + "0x36c4c53bb6b0b8c59e98b35a1eabb5d43b5bc27f", + "0x041da975e077596370dcfd6e67cf546e2c1bedcf", + "0x2e060833573a11e2a06ca467f12dc99aa04fdde1", + "0xce1b76a218f2d93b564b73958aded0e36885c4ab", + "0xe827c9485d095a82f427a4d1a54e781663da2de0", + "0x54b55662901af57b31fb6b52af8175b652a5816e", + "0x7af2fe0509823ae1ed416adbf1f80d452116fcbd", + "0xbfa700edc21a6bf09d22cea1bba95fc54e7b6dc5", + "0xd1f9465e0ae9baa60d4b015ea21d6c957f118896", + "0xc7b8e515c5f0fb34d301313b636b29d866a2e69f", + "0x9b86449941d2f226a2211553f722c76c06d99bbb", + "0x7267db34b6072080923f92e626c2fc5da91ff25b", + "0x27107dd841ea4fd7535c9f7905be3a5d0cad374d", + "0x7b84c9d300551af54f1b415d5bf211f852725dd6", + "0x6512a1c7d144aca84ff06df1e70b84cd8186a949", + "0x54f18b69698d9310e8c645d2b498715907a6ec6e", + "0xded2b63afdc317e226d6a43780de61d013c0acf8", + "0x78ee7f30a9e1a2b1be52e924c9fc7b608177d772", + "0x2f374d4cb2d5aa194c8279d6607d5ebd59493c3a", + "0x9086ffda7e3e34eee95df281d61473bbc2d18f88", + "0x6a4bf9162f59339162530d2fd576a2604212e9f9", + "0x51f059c07e03d330b007b48630a857e582be8236", + "0x8a146531630850fe1d158c922bda620bddf12766", + "0xb92246136f049607423b4c986302d407aef91a17", + "0x459e634907e0722c7145ef56765fbcd924e86303", + "0xe0d01efee7a9740f8e702f086dd4fcae87926abf", + "0x3cdc6f91d41f1738e8e1cbd2a06f64dc6da5b0c0", + "0x92d010aad4fd7eed3f8f1b61701aed63c5d94b9c", + "0x9cdb231cd70b7522c2b43ad18240649f9599f4be", + "0xeda9cf63aea93301b1adcad3c0013fe39cab8cd3", + "0xbb1bf082b1a61131d3345c1f99da32af592da2fb", + "0xdbf4a4874e5bf633d8628e5272f9965ec1ee4dd9", + "0x8a70b9fefd03d8c54877bc26203a87f253266fc8", + "0x7c1a2171553460219fac450d93ed2051f61165b8", + "0x1255e17fff07322231a4124bb455c85d20d0a2c4", + "0x3452c9a1a2f670fbb4eb6bdf91c1ef4e752569af", + "0xf5a945f4fd8633b4573eecc61d5307b267831c90", + "0x4267ca7c40d73ccbb532fb0f2359eba7cf836112", + "0x4ffd0a59a26cb2aa76d403215e4cc2845c053994", + "0x8ef63b525fcef7f8662d98f77f5c9a86ae7dfe09", + "0x2354910d293e778aadf39b16deaadabb5b40d66b", + "0xa9d5e82719626496292be0575badb566551573d7", + "0x49caf2309cbafdfa4ab28d11ae18c3ec9b1cdde0", + "0x7f11beb8887f76d2493d6c18498c27379473c85b", + "0x015cdff2e472f657e8c34b4e88fc60f16f7610ad", + "0xfbab220c53a3e4b45f7275d38139d3eb3e3ff8f9", + "0x08b68eb975a124a895ba94aea12afff4657f4dfa", + "0xea2f509cabc86e09e8b807258d3f9a5f72f0da11", + "0x6386aae7b2de8e9fd895171f24f93d08545fb4f6", + "0xb4773a58bfa5eb5f1029334d5dbde2f12cd35d42", + "0xcd7d70bb7034102ee0c828fbb83776e0137dea8d", + "0xe939d186c9da1f3640a8a53a3df9deb643021ffe", + "0xa8c24c16ad11d008eba1695d37417b47ab461a98", + "0xd54d23200e2bf7cb7a7d6792a55da1377a697799", + "0x8c01b3606b5466389cdba890ea981c23931042b0", + "0x33606226e70b96cf2a07e85401a70661adc3d019", + "0x9d1f79d20b9327b54096ca287f314a2e8b72be36", + "0xdc5b17f7e80ba5f2c06e9d04bd202d394165e093", + "0x2d94dc4d69db8ecda720406c118702c0719b4345", + "0xff3e84ddd3e8809591c7ce584358a5a3f4adccd3", + "0xb2ae7773e14d857d1202cfc2a6a5a2a3de1525f5", + "0x332df51fa1d8a2e83bcbd1069b771d9b632506e8", + "0x59d274ba41a55827062e2e4ad115d8ff758f391b", + "0x9390537c031b3f53c5982a8f40821602ffe3b28e", + "0xf1d508c2ccb2de16b20c2e07869408524e1a8d45", + "0x4334703b0b74e2045926f82f4158a103fce1df4f", + "0xea8d665651d7b3d429fe6bbf6f895456ee2b6b6d", + "0xfa4e885e98aebf061bcc90f6c85880b45e5783ee", + "0xd8ef72f4f03615d94993c4a826eb4deda1f9d645", + "0x9d0a96a24d9c4033c882a3c4a3c33ced4254581b", + "0xe751d56c31dc1b16595a2cdfc94388687e26a674", + "0x8fa4bee3787714f25ea4e6aa16b27438fdc0a5dd", + "0x6a1dc2f320ab982637dc4b67de1c0cfc4ad988b1", + "0xfd1d918196c4586a7d58cb75f1acae330ff5f348", + "0x7b36b298035e9925f46e7d3ad1ca16fe2bbc46c8", + "0x9125b2457479964540a0557e3b010681317b635e", + "0xc78e8e153d926047a07ed9a8ecdf837061f11b86", + "0xdbb3028717e692ef4c002cc7019ae7192bf582c6", + "0xa7eea325c4131396b5ec63a175c9d00bd45a5098", + "0xd996ddb8b48e3728206fbf0bfc91b4378d6c0c51", + "0x5d81d1b2775794e5bce61aa75fcf8778d7d1ac89", + "0x10f73c1eadcf834a33061c4c8a962e528d6ad883", + "0x7b230f8235f3e3b14aefd3ed657479e8ffd931a7", + "0x48be343c86e5d2233e874b23dbf123b0c2f80857", + "0x149ca92a98e41dc669d673bf925085567137b7a4", + "0x104f3dc171f5ce1d1501a49e664ba1fc30252dc5", + "0x13dbd75aefb4ebddea327071257948edba2da6e1", + "0x006fe810cb3830872bcb217dfff18ebf1e33b290", + "0xb4900e989adb821a2989dfb6171af6af225d0ed3", + "0x5cee202b51518b0e4675bbe129f6c9776f4417df", + "0xbb8ade1a31a79422b425e876184724c209f78289", + "0x6ffbb6c376d0cf590289671d652ae92213614828", + "0xe85fea97723d9a67a9cb4bc3e58ffdd6dc3a78af", + "0xf9aeb52bb4ef74e1987dd295e4df326d41d0d0ff", + "0x119030ad7e6fea8f694782898038da2e768cf6bf", + "0x0e011cf6a91dc8c3cdf693b0f8f214760db1fc8a", + "0x0000000b4d325bb539676dac6ec3413d5974cf0f", + "0x34457ca3a154415ee1847d311810b33815dc7497", + "0xfb17d5cd85854b6bee89e714591de521f3169de5", + "0xf90a71d46db26a7a3edcee7ee2c93a10e79eb5cd", + "0x096ebbd5fdc3b1028ebf2279820028c0272412de", + "0xe434109ebbf886fbfc2a364dd7b78bd3d79279bc", + "0x6cb396e01f3afe12e04b1097ac88f20ebcd8d711", + "0x9550bf7df4065a0a0d57340b603478cad4c5d465", + "0xa0f6f7392283163e133bdafe35f8cd7d52520ed1", + "0x7385f8b5ab1303c8e476d371973db768f1a43bb4", + "0x61545187d0d2dceec9261f9c358cab14ac8a4c0f", + "0x92b39b14aa07c309317ea0c1d5a76b2808ef0ae4", + "0x7a9664538a303db562668ba8458ef283f4323019", + "0x81a9769bbac5e53f665c0deb4eacbb0094503810", + "0x469f25c297b2e84898cfb1ce596f2553285f3e9b", + "0x51406df329a3e1145da4da6d79143a3747ed51cf", + "0x73459ef0a33ea718211e91504e9caebec0e6e667", + "0xaf7a0aff1e68b9584c8f41093f9977c6e6297a13", + "0xa07be4744faec9c69a92d654f5d2332fa73bb87c", + "0x99f06631425f01514e774bf3208909fdeb5305b3", + "0xf5bae96e458298d5b723aa350e3bebaf4779cc24", + "0x36d481b3c6ceca782b1fe72a80d704da06e6c063", + "0x43d4cf2fbb008fce9548d94a35c3f318a4940055", + "0x987c63895621fdd6a1e7a6414043b561982a2354", + "0xa9abe4e7cc5aaff81bc02f39d5f2b56a8094c9c3", + "0xdf2dbab86f0cd5ef39af65ce8411f747f36a12bb", + "0xbf1bc5964f7d035f83f159f00209ff4352bd7394", + "0x5fe3661ad3af1a2aa85001364b988cf628c38811", + "0xe2976f612e6220643cf23f02735e7c7b946da315", + "0x6a08ca93567d477c6699c50d3de6eaeb0307c8c0", + "0xc5e81cf721038d636fd089160a1af5575130fd16", + "0x6573948aef66b3eb45f35ab5c7516678f86d7cc2", + "0xce8fc6755efcaf7f85c28901bca4f4b936591542", + "0x103a1f94bc38313af8724777da71201e40adef5a", + "0x625ec8189a41132eb43166929f9c8b20014f74cb", + "0x060e02320fc82a7bf148a443c34920204d56e7bd", + "0xa84842f23737367274b6949e61f0ba8f3239b0bf", + "0x7a505df95aac78f142ca98cecd771d3c398b4b71", + "0x161e82a787324acfacd0d611faf9ad13dfacd65d", + "0xc046f4a055173832866c0010b15698baccb7b7e2", + "0x1c93bda3bef1e486ff31c745278fc3dd0aadd6cc", + "0x4c807964532ff73684740000f891230417eff375", + "0x82767b4b962a2c8d0f60df6e4797d77afb108904", + "0x2c803dca5549862f778cc821ef98c5b907b67caf", + "0x477be27b2085d890df293aa23bcf010363cfb2f2", + "0x4ffda342db6366e564b28e98dcbc48890ae204ac", + "0x52b481e0e149a882dd26167e0510bded8c595330", + "0xfc9e0e4f0359e0972c02b840b9c36e129f82ab25", + "0x3d80f82622b0da34874fdbaef5553a00ca31e623", + "0x22138a0cff84952c018cbcbf5650149017d6b292", + "0x01650a012b91e32a077135bf7c9881c44b9775a5", + "0x491d927fa6c49460446346cc93e7b5e75b0b8c7b", + "0x2c2e209465d5312e6df0cd5f7d1066f1aff9a953", + "0x5ec850d24f1a5fb122cf9b3446f431d1f44a4119", + "0x2c8cddb4ed5aa7d28fcfcc75313de4286e0b5e32", + "0xbfa631508ceef09db02f762f7674bb871108fc43", + "0xd0a2ab6a3bf6904093ec8fc5a8c151e3c4543251", + "0x33b346911a75e8cadb1d3bfa1e9139db59396867", + "0x36d6c29e957fa8e533f1096711cc8d70931f101f", + "0x9084249d04b91988713c24c0eb10d25b90b4325b", + "0x1d28255230a90f4c4df8ab91fb9230c7afebee67", + "0x6fcf4b90fd058aede5c1f37f84940c5d935f0369", + "0xfd5f1397e32506aadae56514bcf6ce4246ecda0c", + "0xdbbe2bb7be24319808e01ddf1ddd9276ad2556f3", + "0xdcfcc77a2e690141432561a9746c125bf6704380", + "0x72c5cd18a51d53db34072546a7a38a4f73600d92", + "0xcc2e601f4e686366b7a0a581eae4d26b0768f4b2", + "0xf17ac82cb2c92853100f150591973857b1b48d7f", + "0xa016b59cea666f53da4da3c9f99b97da1e0b016b", + "0x802cd7ab8e2c7a8e1906063a707f3ff8004ac748", + "0x6547835644105f1313ffd5194f467720837b0c8c", + "0x2d9039cbe219420ab16d31bcb7a37b395f0ce394", + "0x645c22593c232ae78a7ecbac93b38cbac535ef12", + "0xe052b423cbc4e0103ecc7fafb0ec6c0a911df40c", + "0x5e334fc2eec978478e84d17446d842bbd8c5af7d", + "0xd210ae3ea4bb169163f4e9876109a2c204ad3a87", + "0x75aec1d6bdb2d63becfd6c65a01f3e2175b98a62", + "0x1d5156bedd77567e2246ef3b77451b145c77a347", + "0x4413a906a012f06bd0abda5eec45f7d67b449284", + "0x93adaa5757cb116cbe445f41e65108425d5b102e", + "0x6e2ce0d1f8ab9c82be2d0bec89a29fd9af9837c9", + "0x94c4b962b5ad65aa670fec3ae6d6c7b27f8a11f0", + "0xaf184b4cbc73a9ca2f51c4a4d80ed67a2578e9f4", + "0xe0fab13e3a5fc6d7becd6fbc93e36250375491c6", + "0x5aaacedf5a180e084d5ebac2e55ea484cf0d2780", + "0xabd9221a59ba5c03fdd655834f07f44818979698", + "0xc7f56d386d42151018c6dd0ad08efdf6240a618e", + "0x5fb92b742bf451cf55dbc51781a621be9fd225b4", + "0x0c9c3ba64072eb566b0e9a4b6bb0d7b204d68469", + "0xb444776908ff797e69d516caa5cc22856781b0a9", + "0x38b0d0d924e0a060296cc8b39f436ab6b92745f1", + "0x1ecdd6de99277d4228a9de50ea73ba7e10d662e6", + "0xdb0132c875ea7a00c4a6283da592ae6500205396", + "0x2a9c628f9b1b06a881407ae457ffb0e51fb7e752", + "0xaa56c1febdb4c9698cc09b6065efe23443474114", + "0x17561e4a5e16e619e049e3a61a64f2a2564859f8", + "0x5fddaf29681f3acd3ee5d88ef150f066b4db9890", + "0x5c0cb1fbfed1cf91efad1cb3f6ddd55f33665fc3", + "0x5aec24aca614bf525d26087ae41ee3f9c7b0ebee", + "0xb798def92b18861602f956d06fa5cb0bee47ebb0", + "0x8a30fe8dd7c1b8ee6fff8892731ed345b9e33b29", + "0xcd93fc667df20d3ed9b0b80f23a1bf415a9085fd", + "0x1d38dd297f253837ef2b763ba9fe2eece45c48e4", + "0x58c4f03a954e4cbb1b8e204a881a8e9a99d015dd", + "0xbe61858c817a6a17774f8079a5fbf9d1896f3736", + "0x0aec243f82ffd017666f6ffdf9fa81daf2546fb4", + "0xd511a8d9448dc95e774000dcc342207144aa9377", + "0xd3d20f2d89922311f87cd954691102f475833770", + "0x33d835ba506684273a1d1d07f2f64d63de389150", + "0xc2f44e7f63a23a1a3d75146425c3125da3ae24ae", + "0xce08958266f58b638527b224d8b7141a3ff9c77e", + "0xa0a3eae0f173567b9ea2abd8b53099ae09d71114", + "0xf1b1ac4cc89b151de017e06ed7d82436505a6056", + "0x23f1045ca09e9c3206851336a2a05d35bbc812d3", + "0x3381da659df786762a59b658c43c850188808205", + "0x63f19569f0f729f137e1042639bf867eaf8e7b18", + "0x4f00a16d909e3c9df5b5e7be30b2af0b0e67c5a3", + "0x46c4c4781fbcbd501e11ef8d87180c44026a3ec5", + "0x789498ee77215b80d201a814228fa7f248c3be18", + "0x85d38ba18634f64bda3beb3518ddf41796ca2024", + "0x5d02c857e98465f5b3a957b1f43569c4dae58ca0", + "0x1867608e55a862e96e468b51dc6983bca8688f3d", + "0x3e6edba22d9ce22a7168406d26598ae20fe08e41", + "0x01d05b80adf3a8d855a2560a7d89ec32a9f037bc", + "0xa02d5126317c5ce85acd8b0ad1d35f944beb441a", + "0x80b744588fd2fea8ef14386bd953493a097eadb0", + "0x3b3e8c9e5ae7fd7226f55e12c8f5198641f65aea", + "0x43c4ff14dae2fbb389dd94498c3d610a0c69a89d", + "0xf2ba2ead3d26c213721114a02e0c56d8631ae388", + "0x2eef4219055fd3c514ba7b50316f1c3965a53751", + "0x4a4abc2439620b311e6e044aa22841126ceb9cf3", + "0x9bf6c52d4dac02590d6e621f5eddcaa716eaa23a", + "0x6f85f16b35a6bcde77ff085634875ca608c826b4", + "0xacb17fc73cf202ebe532e107cec195d50dee61dd", + "0x4774b0e8c77c06a3fb636243d63eb085b4f6150b", + "0xa6e14452bac0d4cb25f788f1de0af7deca87f514", + "0xce5b6d4f24b06f9c93c42653695a26ab88f1b951", + "0x7a78ee74d3fa77eebf56797f9bb3bf06b6878154", + "0x6fb5421989807f828926b03e44a5ce61d6906ee3", + "0xbfb12c30a347d06778fe9dd582c33c6dedb373ef", + "0x1cf8423299241f155716eb19d0ca65bea4353f03", + "0xec898ca8a76fb722a1db369e69ccdcf02f31f4f4", + "0x21f59001e8543fbd84fcacade38b1a06274f0ca3", + "0xf3e3f3032ef4765dd523d5ea2ce23c36c24dcf36", + "0xb5231e4effc32ae4731d4c6ef65ac58e116c1ef6", + "0xef252a2c56d61d5417bd59e2586ac0adf3ce538e", + "0xee650f5599bd48f27b29b254a4c45322a755c6b4", + "0xe6ff466b9e9259dcd31a9847dc11872d6b066f20", + "0x5eadda0d4c686978982a633fb06910f5a2fccfbe", + "0xd124524b52053c2710549a359e42d96d1acf267d", + "0xe72c1adabeba9f743af60d4deb45bcfa090a5320", + "0xe0a86afb73c0922a50d4a985a25507ecda8a9b51", + "0x09e63215d0d7b0a9d45e1d708a164a5e8c7d8176", + "0x1adcf07389b1f6605c44a7683c50a5243829a92c", + "0xf8253eae286d2b3aaab470b8118e60b86021cb2f", + "0x818eac5be158d2629702051e4b8c24cb27829ab4", + "0x37900736d4baa4fc9930c1a89352391228628c64", + "0x755ce5f31617ded6e41d15cc51d8ff642ce10e9d", + "0x4a7e2122685eea970ee0f5f257c62400d345cafa", + "0x0ea41755ef53f8ecf448b0c19e05255f39e297ef", + "0x352d68686d21237582566c6b95dd7edbf04314da", + "0x4248c2ab690289c5adcedf61981796beaf9c3fd0", + "0x2df32ef622c427ad9a3df6ba76b992622e2e381f", + "0x153b4f4f2c6c284f844a53eb2e097d23b7bfa4dd", + "0x2d7cb9d0f2cf122fd8768c9c7dc30a611243e00d", + "0x19246d4cb92671d0f2166d00c9de687ead3206f8", + "0xd09e3d8cafe386b13792e08752e442669a571f4d", + "0x25da992fd0f68296a2659b69f1642f0d55360993", + "0xaf372b93a02c54e231a9147869ef11f21e0fe82a", + "0xbe7dd82facd47d3598803f44cb980078466aeba3", + "0x11ecfe1260cfe3c4ae27e579dc014ff83482d9cd", + "0xdfb79cf0dd1890f13ef73f5e248b9d61172173c8", + "0x7a921b0365b4571bee00ba5a1dbb422b11f13f9e", + "0xe43fa6cbb7dda0bd863593c5e75fae9b4dcd5cd4", + "0x3ed894dedeea378c0b8e9fe2dbb29e0525496c34", + "0x3214e141bdae08be1f382a885cf3d2a448a5e780", + "0x4ad8c6d982ea20c6b105c480c866fef79dde36db", + "0x9f72b48ec2410586a49c75963f094b23738a0cf4", + "0x1709e403f22189a289fd9e9d43652d6ec92fd2f9", + "0x21bc5e5558e85e1e49589b4b4fab796ee446fed1", + "0x5da318b6f25497beeb5ff3783582a01d27623ccc", + "0x9d497f015fc1f853c82c016af981336ca3ace04c", + "0x259af9a30cd456d0d614e60beb54e15824a85a1b", + "0x99c04ae385e3c247d8af67b193357b79ade10cf2", + "0x5305095cf75cdd312c202e27f6aaf3ca7da79a5e", + "0xed02383d3b21052a02eda1b456f0f154738e7a2d", + "0x83b285e802d76055169b1c5e3bf21702b85b89cb", + "0xf06a70b9248072f7f17492a962557447e0001152", + "0xb5997b4fd73dbd007119fce4e24ed552c1c4cb99", + "0xe5fae1a033ad8cb1355e8f19811380afd15b8bba", + "0xb6b2a09afd721ad5742566fe3f02aca3a2a6017d", + "0x3b647c93e1b60f41ea687f1c9ebc6de3fe52f475", + "0xd8b2b7f42873f111348c835563e26865474337db", + "0x28aef7e702afeda80f05cb8457d49ccebe7f66f2", + "0xdc0d491bd92861746c745b257edfd8cfa631470a", + "0xde5634b9199ca0ca036fec876fc249043dc98fb5", + "0x96e4a0657006891d4ba5fb745cf8587acd1b57f9", + "0xc57ec17511bdcef72947caca8455127aa751c1b2", + "0xc12de812ae612b6d514b52d529f97f6acb524c8e", + "0x003cdd05da057a884803d1ba3f5de8afdacf73ea", + "0xa7ef228999570c8e507e6ce7d7bdb12fe053b7a6", + "0xfa4a7ef5d09f454114ff78e7a8a23eae71bda600", + "0xd56ee5ba5a52e15f309108bdd6247c69b4f624c2", + "0xf28cd05a4d9c5dab90c99983c996343f0dd006e2", + "0xe39516c6ecb2c94e181fb5b6822d70278fc76545", + "0xbe026ae67432efa9ad85b897a70e0bab0dcb1cea", + "0xbd11f25ed4407b289bdd27d0668435a3493a4e02", + "0x1f92b5affd12981ef0fa7ba22a802379fd36929e", + "0xf91b7e024b16468f45f53278609f1b13ebbd5306", + "0xb489400a5237a7cdb38da63ab3eed973a968529d", + "0x51a4047a0f5cbde25846f540aba39d5f451ac885", + "0x0b997e226b63fded87673bc8b43ba24477b8147a", + "0x93617280d57564241b4ad4f4e09e02968f3f2d76", + "0x0ae43d62ab68d0028b54eb67457b7c013d4bf6d1", + "0x173eda92a4e3f1b8edeeb02880787b36736ed937", + "0x1301910a3508e3d5b5549d87073f0d1abf547991", + "0xc14352ad978771051f46a3c8eebd43add56a6899", + "0x2dcc372e20e0c9920b4e2d7c51c80eb92271c209", + "0x40ee2afbfbc4286b25e2a194920c7d9591eef9dc", + "0xd6fd705ee0b31a9300d3e2154bcce777270cbb6f", + "0xd4a214689898eae23f4a36600baf0fff1889fcd1", + "0x708b1ebc569d0b4d127ffd2ac5554efec032f0aa", + "0x5538793f0ed24fd4ea2fcd5225a40995a8f5ac3a", + "0x8056dc8f2fc8962a2a639651b3a969bb6223010d", + "0x2e15d7aa0650de1009710fdd45c3468d75ae1392", + "0x843fb684d6767473965bec92c211c6de690f4b27", + "0x04ce112fc3832fc423dfadd4e344def6a4840fc7", + "0x82e18803b9369c8210390f70cd0237480f7eb1ec", + "0x02ce473377b650b1188778a7e75cd4b31f59d8ac", + "0x56ae42256f47d488f74efe18089ec1e83f0ab9d8", + "0xdc20107902aa222658a135e6f61f905f47998387", + "0x7274a097d392a564622fa8569599d04ff30825db", + "0xb04f18452bb410eb664c03498a481a3c04d6ef76", + "0x4367cc270d68925d9ba89cae2a9c2877f3acd69c", + "0xf09a845c973b8265c8519d503fa6097caab7f932", + "0x1ad4a9c4ca9c0a6b1a066db8447873120f2ff9dd", + "0xa380dace2095adc258d2be066e2a60a9dbacf7ad", + "0xd1f4cf428835a775916dd767e11553b5649eaf87", + "0xcd710d5bfb7a7e85dd19fcf0470eb24ffa057858", + "0xf4f58044b4afbaa53bffd87f2dce77a5f0b32a64", + "0x60ac0b2f9760b24ccd0c6b03d2b9f2e19c283ff9", + "0xdcb25ef40ad51bebc2e63d34e5a15e21f85d212f", + "0x87664be9494ff8b8041e13e338d586e9cf6f1a10", + "0xd83c4ed9ad3aeead49b47af8243d6df6a56b9028", + "0x8397bdf754bc77e08edaea82e88bdb6ee517a82d", + "0xb535d4d9126c58d8da8fe5775088af77de37f5d7", + "0x6ffd435b88902388d5a9c65f2bce6a75e1b14093", + "0x4f4b9492392b88a1d21611575b732428a1bedea6", + "0xd8bc0f7888b26e1c71ff43b487e8c76871d80eb6", + "0xacf6ade9613a4d9b2801a8a490c4bbba5b9f1ca9", + "0x510c5e29e7d7ed78d4a7d81d98bfad8a2a67b45d", + "0x64d4b9b24dc4afb5d3084d7dfb02d5602064e6e6", + "0x5750d70f57658b8a28a8ae659c3f8020b9d4117f", + "0xdbc03531e5cf492aa0f0668618b429858f0d3125", + "0x24438f748083cb8a5f967d6b726c9aeedf78aba3", + "0xddc60d163015cf5ca1369d557a2ca1cc2de1f1e3", + "0x1e9264e33fa02406e41de611962a72653ee24bd6", + "0xb305466dff982283db8886d8b2b54adf88a1c467", + "0x84d0f74d21a89f86b67e9a38d8559d0b4e10f12d", + "0xc166b67c8d94c1c1f9afde897e8ca5d05cd2385c", + "0x123b121bb9771cb171d34c835e92f891759bd79f", + "0x9abeebbe3b9eaa8b2156e9989c86ac923ba10175", + "0xdf3f35bed840a5ffdce0fa383217a19a7c34e0b4", + "0xbeb439195367d87184733badb1f4f26a7df9c576", + "0x29d112c777fa76d1c404502f0e3fdd7b7662e1aa", + "0xbfdd62139f97163d83cf5cb7f940564b56c6fa5d", + "0xc8bf09987727d53deffaf02b2bb1b223ca014bf6", + "0x3967d78660bcbc95c625d58a40c42ce10bd905d6", + "0xac963b7ae5c75f263809c0d8635ac183de9a2a37", + "0xf626e9a2fddbf55b0b1a87c56128a7ba6723a85a", + "0xa3d15d2efdd2a93e5a4bf07d69efe4a142a347f6", + "0xfe3c6993920ac33ba28378a9f92e18de52795117", + "0xa7cfce25aa5d9752c8ef5a86d5d76ffd7faf16f3", + "0xd80267de5ed5de6f7d6c66986b17b138fe2e1220", + "0x88ce30f11723e38ffafa91a22462e57222433467", + "0x7543dd4ee62bb86a04bc133d0edd3f19fb711324", + "0x524c78a7934e56216c4aa20ddc14340127308205", + "0xe1da9e3ea9efc074ebffd4d2bed209b370705188", + "0xedf6bb53c523ce45e94c5447afd7a042678fefb1", + "0x37ac09e1640577e1d71e3787297a56b58f88f0f2", + "0x4836be8c132d02dae41f3c609b109272f5ec1aa8", + "0x5fd3140cbe45241b8ed6e7c4648c2aa8068791f1", + "0xaae31efb56af2d4da4f8b98aee5f269e03d1fce8", + "0x26563eedb269448352ba0663f2ee554fbd389296", + "0x6e28b3331c51a0ddb221ff833d499a2bc439d002", + "0x465fe8f2122966769b3d9d1e7391edf0ac08cb43", + "0xfe701b7793fba9eb4e74f1af5863d84f9d442091", + "0xaf1ecb33e5b49f74c020c0e5285d73504b01e051", + "0x6169143e0f2452bc5c9642ee64ac61a1b6c9211a", + "0x3029087fde42070f36e779467d0780b8a587afc6", + "0xa1ca3a8f81b56407ccf4b8604cd2bec4e005d909", + "0x68cdb7d616a0a2703044bdbc1fac5e03167ae7b0", + "0xa13910b72ce4c31f08c84842ff0a707f1c4db389", + "0x253e11bd6b8e8f302d29622f37d6365a27087306", + "0xbba2379f5cc9a2f248c5cf18ad72379ae2478f42", + "0x3c01182c3bd27df6a200ddf0059d39cc21c3434d", + "0x1a0f3ff515f8a0d353ed7b0eed6292c7560d221e", + "0x6c7286c5ab525ccd92c134c0dcdfddfca018b048", + "0x42e4d76f5b78eab5fc07098fe4b5c35102b5f4a6", + "0xa770c94a39d17075ca9ba359b9ee191127649cde", + "0x4444aaaacdba5580282365e25b16309bd770ce4a", + "0x1e6bd36167620641cc88c846b815ada76f754e5b", + "0x54207bdd0b6b1daa5bd98c0c8c2ebdef65f63a3b", + "0xa049afef83d112f9b9ac4e9d743c50ad08ebee01", + "0xfa537c114edd1d85a6019222c1c0e90707d2088d", + "0xd8855738cf546c6950fe0abfa72cd4a98103f0c2", + "0x95fe3708d68b83ba6a9ea6221d6f64838658eba7", + "0x501c2cae71d9568da1972b03f27ed2d527d20ac1", + "0x9b3fe4109f9ee66f723cbeb353ceb365ae666ac2", + "0x283a2549b640e56901a8ced2ec5277a32cfe6425", + "0x2f843ec4ba0a0c14813ec00fc20f7bd87cc08f63", + "0x0aa14f7de21e9c8ee836d2aa73aec761b34b18a7", + "0xa0f94a872d5736471d50a14cd65c81fe5bf27c79", + "0x0724eb4bec0ba47cfb808edc582eebe79b46b508", + "0x56c1589b0b6899b9d6e9334e98c9d16d8082cd91", + "0x9ace991de0bd8f2e32a357024df85b40283319f2", + "0x0736eb5202248125a869aea2ec3b15a8f0fa2bdc", + "0x12f787d5833b8b65f9637a5d4bbb163b7db531e1", + "0x77c4a515d6779f358d9c70b78c18715ee352cadc", + "0x6b6ca48da27304eeeb1d15d355d948f68b852733", + "0x330c17ccd25839dc9aff158a30ce42959d249ea6", + "0x4d3a307aec8eeea1ac093b1b2f4dca8e23a064ca", + "0xb050e0b02736873bdcb09f1b289afa79744ef33b", + "0xbb49a68c8ea9c2374082b738a7297c28ef3fda26", + "0x8f30c5a59b5284530f57596814b2b2395197e7f4", + "0xe1452baea1efb49489cf642021c86480965c9e03", + "0x629f6a7335531791ebed6e7651634285c3280334", + "0xa8b4c7f8b3d91b324f815252da74884e68fb4c4c", + "0x0fd90eb42ad3726b8e5dd0604d175f2aa9f10aa5", + "0x5e53ce97fcd3abe46f1ab45abb3e9a9c5e19f193", + "0x29f82d09c2afd12f3c10ee49cd713331f4a7228e", + "0xd638dc334f3a9b6692f7aa71a32949f7da12f9d4", + "0x8e789ffeec91c349c6699d3f3a4164e45f59c9cd", + "0x66697dabffe51a73e506f463b27e388512ed6ecd", + "0xfb0873956e94433b4b4ad4f07d6c1fe638f870b9", + "0x01c81c0e0cfd968a96cf08e3dc32f395a9dc075f", + "0x340209c8975508a8b6750c01ae290db038a275c5", + "0xf99c8873b972b4192e789f55ab954188d0d9a133", + "0x1591ffe832548d7ef6152944caaef37fe313381c", + "0x2d13cd9a79e68974993d19451fe615925557fe28", + "0x23c043a293b302e146aa9fcf9a9ca937b895dd56", + "0x28fd174fc197ba46436d2ae835d74934e57c3033", + "0xc766545ef68b49e5c70f4e04c52a9caf27b9a9a6", + "0x24f5ee0991f95328bf74b682f6fad54cff9c832a", + "0x4f555e32e41c6f646ca72a93130fd24be10a8016", + "0x78b127298ffa031f41d1a7b55d1cd6bf02a272a6", + "0x75576e0865f7e517dfe6917c03d70037eece3ba8", + "0x7569ff833ff6c5fdbb58934862a08281828673ea", + "0xa6910bbe603751e19a742154f0986efcf7c062a2", + "0x17d5e950cd12eaaec810dee26d9355710bb22526", + "0xa1a249a46d9334c372540a1919e71210f1197d5a", + "0x6c0d470d97ef1f230eef10a5c848afcc8261ed67", + "0xf2978b5dabb93964410fb0394c07f144706a2df7", + "0x3ebbdd78edb895d8946181e626eadb49d58e62f8", + "0x7e028136aaf176b8e338581c0e62857d8b7f5ef4", + "0x9d0aa99cb5e64d4fc6203906c02ed2409c9e78b5", + "0xb9c0aba138b98656ffea4309bfe2881b0b7c1d96", + "0x9695b507a59f00f2fd667525d9fa661ef0280308", + "0x2f8a99d208b3d09d2a944e27d4451d21eacedc21", + "0xeca88938abff4f6329fe99cf67709b392dd0f7cb", + "0x386d1ab0d50764e135b10247574264bec20f3e03", + "0xae3d9aba194740091edbefc8619a90abacde8fc3", + "0x26feb72790987b6958fcfbf1f53946745f2d3a1d", + "0x3720e6888af0c9f5e3b3197f133924232ade531b", + "0x5a924579ee6d0b8f8172c944a5770625b11b635a", + "0x9400e3b00ad9a7bbedd6912cf92f8fe34adc00bb", + "0x53811bea240edccdd922db6733e5f13bcde1a6ed", + "0x750f53f8eb3024bd63913480a0b47f3e33fefbaf", + "0xf429fbf67a42576fa6d38c310072463e79e2936e", + "0x927154b2a5b19638c2a452e8f74e0b496c2125b7", + "0x03f248f96c6ad8dd0087ff28f0fab1e3205cb3cd", + "0x765fd440bacbd1a34ba08f0ffb823fce62f06c2b", + "0xbeda49bdbdb8abdd6993b8059a8192147fbb55aa", + "0x19db2dea42083d3b30eb89b1c30ac5a7b91de42b", + "0x7a4e47a4e99e7d5eb9bb5a619e4475d1dc7b3ab9", + "0xf89fbc24fe728b699a6c7d05605e2a4b944c6e39", + "0x66cc649d9218bd1ca46373fedd6041bcffb93640", + "0xcf2c2fdc9a5a6fa2fc237dc3f5d14dd9b49f66a3", + "0x7d3a0eaafe6cbf41bcf9c2f091065eac9098e0ef", + "0x2b163ade152bff560206daf5c89ae358e5044199", + "0xe7b80338fe1645af7e6e1d160f538e04241b9288", + "0xb2754b61d93c0c0f00b0be4fa7d2012b6cd2b430", + "0x60305f33437a3956eda0103d4e101021cfaca6e8", + "0x1ee840962f7e8414092a25a216ec94d06e4600d5", + "0x02802e1ae8da338c8a5d220d857d27c49fa86a81", + "0x9f4d78366f3d67abc8a69b44823e4ecb8703ebd3", + "0x9fb470376f12af151c9ce88bf7b4acaa13bad02b", + "0xeff753b70b34ea0db977ecd27c670bf3fda68eed", + "0x94db83f9934adbbf5966a30e55b83e6635c745c5", + "0xec04e0a492f5a5fe631abb4c7dddae8a2c84e57e", + "0xa3615b094d2aa6d93eabb694495cd13efeb9916b", + "0x64d1d62ce19332b7d247b82116b50d6896a0a64f", + "0x15b1835430a1ffbd0dc652aa828a8c2057a82cfc", + "0x61e60af04805d7ddfb0cfde0a96a3b1c15f3748f", + "0xfb563cb2572c50480f0ff1a009da54f1fc5d9af3", + "0x119b134e974e20df79fea161b08970025e4d5325", + "0x2fe95ba46d36e6bbc8d3bbc6bf3f18c85c91f3af", + "0xc8a7403ac82432c8ee1f5ef998874ff3d185573b", + "0x8edaf13697bcc248a428df216b42610f9af5df54", + "0x3df5d15722e8bf01af9fd19c2e2e5fd3934357f6", + "0xa24b12943dd600052ba36a2543f3598c5b6baf3b", + "0xe65ee5600293aee2b70a60a48cd44a492ebb6974", + "0xc8f64d227c715ae078d642c0e3a15d44fe7a48b1", + "0x445726b59a24628295cf52e8ddfd3617b94e3932", + "0x2ca99dbca3b8922b6b4be71554e543254f11b0c1", + "0x62371b45d283e13ec4c6666d3f8f0aea06db78e2", + "0xd699571a57d3efe7c50369fb5350448fa1ad246e", + "0xeccb9b9c6fb7590a4d0588953b3170a1a84e3341", + "0x0d0c4d8bc5029e8420e1fab78d1373ea175f4bae", + "0x3587b15f7865d4f3f5ca15d29d197bb2f1e6309d", + "0x4f725e7cd53d76af3dde36bd05dd06292a6f3c56", + "0x3a28f433f23e17a6201552b5076869eea077b6f5", + "0xcb6222f4df04385ea08e8da2a5871131ff5f6cba", + "0x0fcfe3c0d30b88b9b06f85db087439c524369e51", + "0x0ca1ed3c27687a46dc287dc6ebb4974ae5a0c3d9", + "0x5e4eae80115a58e3c1fbacaa82107ef039e3ac6b", + "0x8016ad7a57fd087679c57e766d0cc896bf43615d", + "0x9e17ed28b590d0b422552b961aa685ce230ae931", + "0xd2cbb2efc68c8f7819807eae033cd5c84af8e235", + "0x03ffdb95d3ee7f7b796069867e55cf6fdb65472c", + "0xa6a2ad1320cec227f24f404fccc120b71b41757d", + "0x9100ffdf05ea6898002caf643d3bd0433abc1d15", + "0xf0de256d741910bbe86173f02c795e1034cda0db", + "0x82ab77185a80a5b74c92d679a02b28b115f21962", + "0x174e070920007350fa0193e0321f9bbd3984949e", + "0x078b7b9520514eec7e767574e5f0bf33c84d910a", + "0xa7eef766b1f3f3d31a0c7c6ebd0b3f31e9a87634", + "0xc87c7a63fce82e5929524eebff820a16fdca1147", + "0xdd043889435603f2b99a64b99a065711af3c02c9", + "0x24e1c10d4765ee61840aee6ca38088023ed67435", + "0xe0e4bff7ed1cd39dfb4310eebf078187586526c8", + "0x004ab0f6171ff2d5cb44b6d50cf8eb2edc56105d", + "0x02aa0b826c7ba6386ddbe04c0a8715a1c0a16b24", + "0x8bf48230d4c10f011c4d1b73804e20436c239902", + "0x3be960f410100916b49c41bb8fc52eb5511ef307", + "0x4809ecb2fcc97477e86c0cd1499e9a102fad3507", + "0x2ec9566fede31ef04d9a5d4ff9dbeaeab50c5273", + "0x4b4f333e18dcc441d006ebd9bcbf5ef39d7ca1ce", + "0x32477e70b660272f514978185cf14a0c6fc68019", + "0x50f527a8f3898bc4b26f5b032ee60c5fc906a2c9", + "0x3f52d38ca3a23c67463f822854d95ee8481ededc", + "0xcbd9f0c0639fc515ef64835354935ef14fbf48a4", + "0x2b597e53bef7a9275429a3d5443fcaf0ab12c26a", + "0xce29cec7552ab3c9cf5264ed96081c2804c3ef07", + "0x2c42346820795917e7d11579affa976b5a571573", + "0x064c3bf2b2a26062ac038b95aa2b223cc8968040", + "0x0c6a0b7dd1063ae7443ab562622bdb4603dddaec", + "0xa0a8338295e07e11f118ddc8e3f604b84cdf791d", + "0x52a950d98f11fd9edce6692fee7cc8e110a471dc", + "0xf93ea91d48e3fcb4a9d710c6493b4f6bccbe4943", + "0x043f1d06806bb1de6acaff98b078da1a6aea55e5", + "0xbe7019f8333b0b3cfc707ac353ebc2123140134c", + "0xcc731095cd449eabe6b02a313d78fd65f266cc29", + "0x67c4064fd8cd5693458d5efdcbf93dbf4036384f", + "0x9aaed9fbf4dab79296190817b85645d3c9dcbe7c", + "0x538f28b757fc0412a250491e586838e2ea240c54", + "0x32a534bb6afb78fcce6ba6aeb394b148e1c398e4", + "0xb7deb9804764294d7fb5296bb81519f340247c03", + "0x585491bdfe0e82edc877f7ba5f9294dcb267c093", + "0x4b240321bd08aac0b0c2ec441d5bb1f7190ca0a8", + "0x643e1004dcb9b37d00442438f69d306473fdf58d", + "0xe1518a5ae4c762a0dd7773b987f6bd8bd7ca155c", + "0x5ac2bf9d420505e61a601143bb879ad02f3a3746", + "0x41a6ac7f4e4dbffeb934f95f1db58b68c76dc4df", + "0x8fcf51b7562f2204518cbb8d678786ce09b4106b", + "0x7d0b45dbef18ad95633375236336a9e7b9af7d76", + "0x1a2b292661f0102c5961afc42b278fe8c9169234", + "0x69ff5358fae4bcc2ec6db6cdb30d43d129291709", + "0x1d9c68cfa5fdfb6c585bf01f715d359f3039b63a", + "0x9050c755691e9089e08adee29462bab6b338def0", + "0x92ff028954f615c00ac555f2e852d0e1b85eb5b3", + "0x45b6209b736912e6c4d12d3650b582d581b28978", + "0x8a806bc475331f20022d897e4f9dc066bf0324c8", + "0x896d723154eca634e5dd8a5d3ee76a4f1e9be22e", + "0xd7e17834f4aef24f732dbb6f0d364ad5fde9d516", + "0xfecee8e7642a25e6a07e0f5ef4c09cd9557f6bb6", + "0xc19ed1fa9fa3968fe12e97aa4d511ab6bce1320f", + "0x38476bc1deb12f317cabc3f49fbbb80fbab6f3ec", + "0xcc915b7884209e7c6fab158f57f854517f4a7bd8", + "0x6c3c301bb3af46c86205844c7ad9ef8bd6593baf", + "0xbdf9414544ffc5a4a72029ac6511b63cc00f224e", + "0x825fdbceee8856cb55efd3b0128abf386ece2315", + "0xdf42507b9656faea7deb14b8d409f84ed3542dff", + "0x5fc026ab7f7c6ac62c62e4382f7ff3d37e2c2a75", + "0xc677e71b02adc94534de993a907352f748d21143", + "0xb5f69d0cbd702d713767c5442122c156bd6c971c", + "0xd62c7913aa2b190ba54af680c92665c88d6f89b4", + "0x74fc147dcacdf2680c3219c80191121f2ef2258b", + "0xa12eec92d8c155c8ea9159cb87da4e062bf9992d", + "0x933b29e605d3a5461849798ddf0a39100a4629cd", + "0x1a9836f6dcc253332ee707bd9808ab3d79d973d5", + "0x5a0a4e95f30facd178c396199a9f37b6c9ef6b81", + "0x7a69d06fd825fa64ca81cf8dc9710abd7cf05c24", + "0xf73bd5710d3e3d4e8db7ec6058f9fb77ff8414e9", + "0xaddadf3df10bebfff7201427d50cc0448e2e6f3a", + "0x0b36518f2da8c357f235be18b0b28392b45cd1ce", + "0x28b791300024fbce569ab163588cefc7a5cdf546", + "0xb005fc539bbf699cf72af82343e0da5da5fcffde", + "0x98fa18f31bbfb643f47fc866262aac55c9824917", + "0x27f0d8cced5f180a330ead4cd11d4acb1e42fcea", + "0x9c1dcb699e6af3ae59b7219e5ea7fef6d9fcf9c3", + "0xf4a2f32befe6ebe65bc2696969cd1c1a34e0f92c", + "0x85312d6a50928f3ffc7a192444601e6e04a428a2", + "0x708df04e03ac02c440937be6e631fa8b46cb89bc", + "0x0f45156f109e474295913d78036fa213b1745d5a", + "0x8d32b6272a22defe432100a57d952f7e8533692b", + "0xa628114d249ff3de888c9076a2ce370175e50617", + "0x28aa4f9ffe21365473b64c161b566c3cdead0108", + "0xf147e390aabaa560c34d44aa84510858c476a677", + "0x0a50f5f6301d747e2c67e26aaedc38b9469a8db7", + "0x44f4da18d1e9609e13b3d10cd091e3836c69bff2", + "0x6458559c5489f3b9274de69294b8c1abdda867cc", + "0xca7c767854b1e7305de07247ab85e30543d1d9c9", + "0xfc3267cc1ead34059f43a70eda456b29d1d64214", + "0x06ac42915dafd5166505377713b849ad85b56a87", + "0xc6387e937bcef8de3334f80edc623275d42457ff", + "0x0403ffed505e4fdf9bb13309bec69eaa803b06e6", + "0x19799f272574079a8dd591c13f7aed0d2f7aa011", + "0x7d9f51490414a144a143f31fedc434fd96052af9", + "0xb02dc413e90867f035fa6d29fb54a1e9ab7b274b", + "0xe175c44341736349fcb4c1498eb27d77b96d7bb3", + "0xff9e5e8cb270d80c2eb6059c6b0d70a9aec96922", + "0x7682161c9fbe3a4065b38c2cd3ca61263fe27769", + "0xc9478772653fc70f52e60c529aa9de99f4ef95e2", + "0x904beccbdde4436696f14d846008818495aa616c", + "0x353d566af2b571f6ade5cc9f7a91422f6f738098", + "0xe4b7fbc0c55299db43553908c36c98afd641d3cf", + "0x072e333e4c5b434a59e74f8876035cf81e5f248e", + "0xa3e17ca80549c16cc850d37acee1a02c45736f68", + "0x6ba58cd30014a861b11ed429200bd1dd8277dcf7", + "0x5b79264f6c65aa1f6b447aecdffdbfeb8d71c18b", + "0x39f9bc86f9fd445403fe1698708e76e86078ff23", + "0xe379ad19528a01de884a7ca6a7fe480dd157c182", + "0x99298b15b5dda538c67f08af804d85e82450ce39", + "0x648cf383a4fe746a0e507fbb522e6ecbffc8ec25", + "0xd77ee49bad638da26775779e830339dc3f61ede1", + "0x229a933de3d977048e07afc630590a91ab88c47b", + "0x69f8d754c5f4f73aad00f3c22eafb77aa57ff1bc", + "0x44a787f30b7b779315e13843a877284a708b879e", + "0x8581def657860bf58a5b0d6d2d357b51b81655b6", + "0xe88a832323ed7b3d29def7eb357ac308df3673c4", + "0xb1bd394c25f653b71f56f66df4a54653dcbcaca2", + "0x029fcd8016fc878a674ff8d7302a541fda8945b7", + "0x6fc63168323527883d7cad0d6b4bc51932c22ed3", + "0x58ef5039e1c799c4c2a454fae28a2dee21916e44", + "0xb4c93d0a04d4b7966776cc2a0ba31b2c9bd40d18", + "0x827c4243634e99b146c9fc73cf703af5e5d183cc", + "0x838998f5ec821abf63f17c72025e5d2e277344f5", + "0xcb0bbf3c0a6af3788c6d1b108ec31937c2779df9", + "0x85d5f9aa64848677b5972603a04a0ffb2f321038", + "0xeb3aeec75f1c6f0e3525c0e9d7ebd5ed37a2b8fb", + "0x3ca99a967825891e2ae4f5f05367ad7fa83f22a9", + "0x589ffaeab4a99275660a9fa4274cb58f7329f4df", + "0x9aae12dd452f78ae8919c2168717e7348392ad11", + "0xf6f441a3784fdbcfcaefaea6881571ca1ea625a2", + "0xbca701b4ffa41a73b483ebd1072e8c75bd4661ed", + "0xabf124216ebc84f1da154e5e920a7ce6269535a1", + "0x5d8d5184b35f0569b79d7e169be0aeee21a3c078", + "0x5be138179a1516e24cb13ec34112a9c4dab7b384", + "0xf389994cfe868f531c09a9f27ca9470620d8686e", + "0x1da29ca4e330f0faa816edb6b55b46f9ee453f7f", + "0x8a42c32e61242b8c7a69c34762f35e5142a016f8", + "0x15df788d6a0253b91cc2694ee6665420247e381e", + "0xdf46bfa73936458e4bc90e3d1a1cb60af7e55672", + "0x6e88f95c51aebdac258994490d133c95c871b10a", + "0x414d5de6faf79b80069212efa523c9f8e825702f", + "0x8e77b367fe90d91b3f56be198ded811b74c82be3", + "0xe399ed9a36dcd2f39ce0398eacca536c4f2d9d1d", + "0x78fe849a5e90a7f5eade2b4dc8b443f883946d5b", + "0x6608673b607c5f648c347014df5ab1a492d7281a", + "0x205043ddfa3f742bcca920332ba1e6970701cfce", + "0x0ff324d42acdcf93ed7187cb9705b4609b074bb4", + "0xe55c38718aaa0e777f7c6057070a89134418c67f", + "0x21b8d5cebf0df88a151c50bcc06465b5e99fcfdb", + "0x16ce1b15ed1278921d7cae34bf60a81227cfc295", + "0x6542fd1b7d7584f446b13564b0fe5214c8b85bf7", + "0x2ea2ce595bd68a0cd1fd34b06df98970952fe4d2", + "0xf8e98a6539545590f70ea8d46d5147118ee139e6", + "0x920ecaade776b466a550ee3fa6dc860f1a78f78a", + "0x79e21f66b9813f7922fd56e14051bbba83eef96d", + "0x6903e7e1b1d0b67a6cddedd59d4402eee2be3797", + "0x559c027192f28c0aa3f9f531324c7dadfacead82", + "0xa0b427aaf79bac6031a2cd2b7babe1d4f827aa04", + "0xbe782ec98ce7135a056e31300a9831fa35d52865", + "0x62504bd6b512aaed4f6be3d61b017eafdfd3964f", + "0xc4c6e038c1a126c57f540a4c5d57f8b02d05fda0", + "0xd04f2ae9b9c0a5a80812e901733d799d66c78b9c", + "0x509dcc51971b5b77b91585553f3e7c2527c92460", + "0xc3dcd2eb5d52fa4a870a69e63350ecb1248066e0", + "0x906f186f2398949ff16183d1ccd47796d75204c1", + "0xa4a62f7127c644aaef733029e0d052e411fdafb4", + "0x9ce6e6b60c894d1df9bc3d9d6cc969b79fb176b7", + "0x21d9c1d2cdcb44b04e6ded64dd89bcf5fcb09ce5", + "0xa05e21550eb86462388783047da318d2de20bbda", + "0x8008145df53e1db0f22737523b029320f6922e41", + "0x4538da8bc4108a1f1c20ea04fa1ed33bd141d3b1", + "0x1b47235150bcccd6b51f60f175174dd2d579bc13", + "0xd9a62b4938ccec820445009ab91b71cd1fc6cff2", + "0xddebbf1bb417e4a2ce7e1e0ec82abd77a899de51", + "0xf64a67a81a0f9bdb67fa232918859eb6c640dd4d", + "0x61c5cdcde2150a3b6c9cb27d14fc0af0f42af5c6", + "0xfb4dd74260f74ffdeb141a1a8f209710c5ec9d07", + "0x98a529e3488c2d44566881feab335b17b1c3b430", + "0x08d97f4c2f0225058d096f7f6b637007efee2880", + "0xe4e8301296bd883515d539490105b1c797c52fec", + "0x51f9fae0199f65445c9c3d2429c1a5672ce5b226", + "0x04bab032593c39c7d600dbb0e617136095db47fb", + "0x062b0613a91603975b330c4fa4bbd17feeefd130", + "0x22f851bcf7353d072ed5cce3caff096195d2794e", + "0x0feb0f31d67adbbc950ac0c5266490a9a9735a49", + "0x06da2826dfa8d4473dd2a87dd28fd186c81b0073", + "0xe2ff239cbec4db2bb8273b4e7c4f1e531794d2b8", + "0xb9999d5f3aacc5c92e1ec4fbd31139a91b24fb08", + "0x97f16b00d436fcd49d5911e68002f1cd4d5e47c5", + "0x2e792c5e7827e8b6bb26761e2c4c44b867838166", + "0x99ae3f791eb80db2a96a1885793c5004273cc7af", + "0x8caef18a4e4c68a1132ae195e628ef68043ccb05", + "0x3bce96ab4cabd0b8514e42a84a074403413863a8", + "0x3b2cbbe7fbc39846cd15333b0e2f3fb89eeade2b", + "0x94bb64f974b5cc1aeeafaf66ac43c6195d6e7bb2", + "0x836e71f16a53577edd2ea377a8ab29a90ae0cdc0", + "0x09db9b873db84ada9059630f79cd832b4d576800", + "0xa0fa13f542c1f45facc537f8ba8db3c4f91a4fd9", + "0x0b672f8eadc1fb93ce72ba2966a32533d91c7293", + "0x56b32744bb8aae79f34e7701e581cb6e2136e0d3", + "0x9faa6a2cd5385fc277498254e535eb617ffd0c63", + "0xed34101ec8916fe669407777023e71a70c462031", + "0x1289c5314b7524491418fa5200ef2f966a3a923b", + "0xed39c5e2d4b2fd31e0f9a62fa5416bab8c062a3c", + "0xe6445adff4178693ab0abe22ec1e5f44841d1e3f", + "0x901ab2d7ebe247f2247df9cc090f81714920e0ec", + "0x79a20d3b60e7f397d6c870c21335decb105fb1c0", + "0x2d03391bea073095eacc4785aad7e63007603dd5", + "0x7644fef96321e78d0158b7b432ab9c58122f319f", + "0xcbd6b0dee49eea88a3343ff4e5a2423586b4c1d6", + "0x6a5334e6c9352c1b5735969dd439e41bcf9b4da4", + "0x54b325783095132a996027a622a5b4cadc8be876", + "0x26c659e023e522264455374f82758e365994c420", + "0xd6339759848e5cef0a90fb6bc3406a62315739c6", + "0x941689a3677ae5d7547aa6b298773db1a50e2ebc", + "0xd4fe43adb09b3753a846b6818a0a50ea8e4ff188", + "0x984f47fec079d3349bb3abdf05e40720e1c2d968", + "0xf62bdafc4cb3808346c5854c0fd129f2a22c02e0", + "0x7b3fc9597f146f0a80fc26db0ddf62c04ea89740", + "0x383697a7fef14483920ba9be57c26115383d1dda", + "0x82d1883ca96e57773429e785f195e32783b1c246", + "0x84669952f2b6309b7876265e7037795f5958b388", + "0x7adfccfd3691a99530f632eec3dc480419addd7d", + "0x3714e1078cb6f67ef01442b4213012b16889926f", + "0x714be2aafd387bf46cf5b9d48d596a2b851e5681", + "0x8234ac0eee3b4b77ef94e4cf2681107d19025562", + "0xd79b646031ae71f77de7a50cc5f189f5c62b7a5d", + "0x55af604e03beb3457dd754d2f630eedda6a1fcef", + "0x9454f17a6bcc36cfbc8a07011b33dafcebe4050b", + "0x83d880ec71546746d24e7fa28e1ea3964e236d82", + "0xf116569b3f888d639372a5485685a6d8ee28a593", + "0x69b8a24edc9053b3d007710e7b986df40a0bc7eb", + "0xbea645da9c7f6de1f7e287759ccbfd5ab9f57c08", + "0x59d2e0a2be0db463c36038a5fd2535f58ab3b38f", + "0xda7ece710c262a825b4d255e50f139fa17fd401f", + "0x3b82b31bbe0dc3fe0f13d17f9a4d0455ef562e06", + "0x19c4e72f327ccc3b47e6749dcc3920dc232c8ddf", + "0x62058f928e0b8af1279670437d7dfd9e55e34560", + "0xc766d73be01caa06834032bf5c8f8f0f41a7bf36", + "0xe8996228b2a7a7937af6ecb2bee5b99baba6af3c", + "0x181a15c71a37b4b829477dfb7cf006f0493b6f86", + "0x612b8e8762bf14f445467d023f9f04c0c22d3dbb", + "0x7afdd1ba1145e583fba788cc21da686e736a311c", + "0x297b84479d7a6fa254e13ff8af9a39a7b5d4d629", + "0x605088609f9ac2f1f4571c4ed57a9318dd381f0a", + "0xda294570098d1fe503beac1ebb63980bfc48f4d2", + "0xc6a8d31bdc07860f12e22c615c103400b28176b3", + "0x2346fef37a4f36753594916be2abb15d2566c4ca", + "0x3dc56e85283ca780d5fbda7337b6bc98a5e4f22f", + "0x32066c25ece53f6844b35c8ff155de36b40550d2", + "0xff9c6e3acc07792649644e97e358ec60ad22b018", + "0x9ee30b8d06017d579752c19feee719d2aaf59265", + "0xf38b07b8ac72ad70806e902c2ecfb7edd36ca3f5", + "0xbbc711eb29527e2b86049d41b867cf009d0f1453", + "0x3d7f42a6bc03ed9dd42b9853ea027eb5c844a36b", + "0xb0720a40d6335df0ac90ff9e4b755217632ca78c", + "0xc345c4d05c0ecb55a2946ba0d2bc2e737bc9a2ec", + "0x6fe4aced57ae0b50d14229f3d40617c8b7d2f2e1", + "0x6f199f00655e0d3e9c1f2b654570758ca1e33759", + "0xdd1abfa095818d4a26c02e8da45caa5291526ada", + "0x8ff6fb658244bdf66c051fa54db6768aba552e87", + "0xa219712cc2aaa5aa98ccf2a7ba055231f1752323", + "0x258302d09995a71b185097738a46ec2095b9197f", + "0xe11925dddded86a78552ddf7100b451cf22743b4", + "0x942c9a79866ebf5d6f24e6b308820541da6709c6", + "0x2450d8083b5ec7c2e9c1122085aa23fa3539c64e", + "0x7c6b59f52578a1bea4f8d750c5b4bb044669b5cb", + "0xcbebef2dbafec16529277be933d60cb0524ade9a", + "0x2ad8ae2e1421b1d0a0823c0e8f71d40ceac4f872", + "0x3d2b23962ebcc882f9f65452658bbba9fa72d170", + "0xc8a44e444e7dac7744bf2fa35256f7300e9c04fb", + "0x63d293a7f2ac5443cad758fb481ab32442d4259d", + "0x53eb9a6f34e1fead9cf1013c7058679bca6acc43", + "0x25e7fc427e81e2819fa9c455131c1f857e42a81f", + "0x106bf19d7f44dfd1e4d41b0890e568ad7fb5e511", + "0x710969f07f9b94428a612e26dcdf7728524b01d1", + "0xda10f11ba76503a2d9e3f29d4b92b6dc3417bbee", + "0x8a46c80e07f946662e80c237a3ce07be60a42d61", + "0x70a13e91621a580bc6773e8d27dff896ba6d76d3", + "0x214138492f53fde449bfb4e91b91e3e1ba342264", + "0xa38f91aeaf08a8e8a9e488dced678099d688900e", + "0x8f35df47832dc2015011d0af1d5f1abd6f7a905f", + "0x09cfb07583c610fecc6780254d354cc286f496e6", + "0x8a6a3fe565451f72876d88ba55c6152bbb0c1836", + "0x841382062d00f024a7a4fee09e42e08df41dd714", + "0xc9b7411fd802bdfa055cfd469d1aecac2ac64394", + "0x767504c377dcd3985a55760e7c20599bfb21291d", + "0xf102b03611e104cadeeb7605ff1d9fa5c07ba115", + "0xcfaace98e68ca2e796ce396760515606fc6855ae", + "0xee1cd2cd8906a5e03b879fd1a05e9d08e592125c", + "0xa51b7c8d109b0aee51f8c64165cc170354b9323b", + "0x37633c9f778c44e852914c7056cbbbf75323dfd6", + "0x49e59de5dbf06ed83116afaa0570bfe13a8d5ba7", + "0xf59cda0eb96d6ddcd12480bc31c9abb75ebf1cc6", + "0x88023c305b471649d326b7b95149146f4b5308b5", + "0x8fcc6548de7c4a1e5f5c196de1b62c423e61cdaf", + "0x8ea12bc08b5cb9e0d9a38c90d98b35afd966c3c5", + "0x170a426c949caf6248df60a9b412eb3df9d32484", + "0x95a3d06fe7b39399e2edcc31435a235585fec852", + "0xc817eff12fd883b20b9fd91df00b029390ec1ffb", + "0x104ba5ab64e949e05ea77d398deb30d16c713b9d", + "0xfe2fb8587760c8d5960cb7a5ba2f2299edf10506", + "0xf0eee1c6cd279aa9f8c87e8befe0b688440db1c7", + "0xf43257df6b2066c866ceee61218ee1ee50d3be6f", + "0x825736e7336232cc595c345d049474226fdd154e", + "0x4b7292679d439ccd83b972a5ff848b47391affca", + "0x0422211c02fef3f623b1da67a116482b029d8bca", + "0xd08b4f65cd97123348f41e684f11e65bc510157d", + "0xaccb019890bf879d19f5aab1db068057e4cea0f4", + "0x6a087212b8b9fed39e0874bd37a5236236ee8acd", + "0x88e64d1eb51d4dc955568900097f01d6af953a3e", + "0x67ba1c914e6b3b396346a6498c1ef8e59802b1e9", + "0x0dd16908663523e03c11c0dc14fa4276c1010a6b", + "0x4bba887158645192dac8abea73d6aaebab2b3b13", + "0xb334c11d1612231b60aee4048fc6b57c3ac91f3c", + "0xd8fa3942e3ce77b6f98fb17b2564d273c0b67d1b", + "0x7c895f59b8eb7fb88ce41bf03156650ac40e16f6", + "0x2a4e02673c3a50c4d5cb1c372a3654e465d933a3", + "0x1e57d64a5abc4769afe6c6c01f58f666e219f167", + "0x0fe1aa632a2837239f218e3efad21e8018a9f4ef", + "0x3838c954d0629918578847378ee22e6778473239", + "0xbeff46f500c3dd2a6301b9d869fc49b47302327c", + "0x84807bf486a2ec97aa667c6dbc68f73d587ba150", + "0xc07686af7aee9353d8e88febfbd72ec68411cccd", + "0xaca3be620ab0378f551531fce89006a358b84eda", + "0xc655b790ff812109c8f6c3f24fd20b3495164a51", + "0x219ee0c53d8d8cda8700647b1e2a58896a4dc5f1", + "0xe476dec76e2cc238633d1e361b5a7155f46099a3", + "0xfe81028185b4a2465751f181bd8d2fd318d460b2", + "0xd01c4e5e9681f348b9ce2135ccb2e871081cb239", + "0x02beba6e034d547c685fd496dd1ea783c355fe7d", + "0x3c9ee43b96bd5d3a7060ace8c98d75736a1ebc67", + "0xdaa8cb6ab1c9affcc012a5a023ef1b79f91c6297", + "0x3a39899dc78b7307acd83f51463c853fa79e1b09", + "0x27936eceefd9f1e30c20f13f7bc91ccdcfc2d907", + "0xa2863fccdd9ef5d378374ea6a6db5708654f0056", + "0x5957145fab56dfe5af988a4f55eda4b205922c1d", + "0xe773558e6b7c2c27c35550b29d1d337151321986", + "0x15e20bf6f80f6ca69cdc6f03483e2d392860eed3", + "0x60a2e0ddac932ac61e38b9ca80aa75926966e01d", + "0x6403c6a27171349324307919039c452910eb3452", + "0xd3d4f998b66f0dbafe5645d14f001e6852271daa", + "0xe9c2f269ac049c6d2c3f893eb5465f8b33e561fc", + "0x07eac6dc2af876d2f0402f6804cc6ceaa0cd20cd", + "0x8322961c710571556ec8bc91554f675cd1828e85", + "0x59dfe67119fe222f4a5485b15c6f0b4644e43e8e", + "0xfd20125df89e8ffe237b5bca38acdf3cc54ad5b4", + "0x93a4a366de322dcfc68b629d6086c6b19be4aece", + "0xa294a7623f538ef9c0497fa8366a1d27ee46c3a7", + "0x3acbc3eb173fb19a3681c6539a1e108bb4cfad8b", + "0xf903672ba62039591812141eecb4cfd9416cfc83", + "0x79be4bae05714b8f50c840a2b4bd4d8b52f4233f", + "0xda4c20bb77eea27a47d27adabf2071328d93a67a", + "0x62b2b7d015d314e17a4b61553ddc6d205c0ae2c5", + "0xe49f9c282fe9c9bec52905cf11fbb93a59568fd9", + "0x527465920de22ad83bae0043f0bf975d52c9c63c", + "0x0ff51f912eea28b6195c2db43e0e90ba905b5b42", + "0xb3a81311c3a178928cf96f5092d627a259dc5ad5", + "0x88ec8e63db2f705e518e628bcb6cbbb0a4f170b8", + "0xd99591a00a7ad4b5c1bc560edd219f7749acf2d2", + "0x47c89b9a82a508cd5906d24d613ecc755cf4bd41", + "0x1ec9ce39b453ff37bf21ad441fc4060efa90c4d4", + "0x875733f4c30bff1fa1ecc78045cee4b8a15ff08e", + "0x44af58e9ee170bff3f09b224110620dce521dbf7", + "0x544a40955ba1c7e56e161a59e1319e3313c25251", + "0x5192fa6618869ffd9fe6230d94b0e4c8f57adcb2", + "0xae9a5f08dcdd84aa4579e0870a07b62aff19e4e8", + "0x911a182a475c0b54b6a861c77cb5a8a1e69b482b", + "0x69e039d6010bddf601271c2b51e126f987850c68", + "0x43dfcd47e7c5c0059840883d9adde4149c8c30ff", + "0x08d8aebde1a4fe3ec84678d4dba286762518803a", + "0x71b700181b31747f28a7cde2c26b503a2335c0ad", + "0x6b9e4f287d44cea39cd8ccedaa42488617aa21a7", + "0xa60aadf62907bf06dd714781d44bbd4c2d783259", + "0x19879714826aab01a7cc90eb7c254177c460a36a", + "0xbd65e3e2d82430ff414685cd5b4ddadfdc8b0686" + ], + "Available": [ + "700000000000000000000", + "1400000000000000000000", + "3500000000000000000000", + "3500000000000000000000", + "3500000000000000000000", + "52500000000000000000000", + "72380113299376700000000", + "77700000000000000000000", + "53499600000000000000000", + "56999600000000000000000", + "63700000000000000000000", + "3500000000000000000000", + "2100000000000000000000", + "700000000000000000000", + "700000000000000000000", + "283500000000000000000000", + "1050000000000000000000", + "7000000000000000000000", + "700000000000000000000", + "217727634222945000000000", + "7000000000000000000000", + "1050000000000000000000", + "24999800000000000000000", + "164999800000000000000000", + "700000000000000000000", + "700000000000000000000", + "142100000000000000000000", + "700000000000000000000", + "5950000000000000000000", + "700000000000000000000", + "4200000000000000000000", + "700000000000000000000", + "105499800000000000000000", + "21000000000000000000000", + "1400000000000000000000", + "2100000000000000000000", + "4200000000000000000000", + "4900000000000000000000", + "4200000000000000000000", + "43549800000000000000000", + "700000000000000000000", + "700000000000000000000", + "700000000000000000000", + "700000000000000000000", + "3500000000000000000000", + "7000000000000000000000", + "1750000000000000000000", + "3500000000000000000000", + "700000000000000000000", + "700000000000000000000", + "700000000000000000000", + "4900000000000000000000", + "700000000000000000000", + "7000000000000000000000", + "145360492537874000000000", + "77499800000000000000000", + "3500000000000000000000", + "10850000000000000000000", + "700000000000000000000", + "700000000000000000000", + "1400000000000000000000", + "7000000000000000000000", + "700000000000000000000", + "564738350252212000000000", + "24168943557079600000000000", + "3500000000000000000000", + "700000000000000000000", + "3500000000000000000000", + "14784147159402400000000", + "24999800000000000000000", + "2100000000000000000000", + "700000000000000000000", + "10500000000000000000000", + "1750000000000000000000", + "3500000000000000000000", + "7000000000000000000000", + "700000000000000000000", + "7000000000000000000000", + "4550000000000000000000", + "700000000000000000000", + "7700000000000000000000", + "53499600000000000000000", + "7700000000000000000000", + "1400000000000000000000", + "51427810848266600000000", + "16100000000000000000000", + "16100000000000000000000", + "53900000000000000000000", + "182000000000000000000000", + "7000000000000000000000", + "1750000000000000000000", + "700000000000000000000", + "7700000000000000000000", + "3500000000000000000000", + "7000000000000000000000", + "3500000000000000000000", + "700000000000000000000", + "1400000000000000000000", + "40399800000000000000000", + "23100000000000000000000", + "228965423923561000000000", + "23789282320921700000000", + "3500000000000000000000", + "7962834703943520000000000", + "8750000000000000000000", + "7700000000000000000000", + "4900000000000000000000", + "75899600000000000000000", + "22750000000000000000000", + "147000000000000000000000", + "700000000000000000000", + "24999800000000000000000", + "49999600000000000000000", + "700000000000000000000", + "21350000000000000000000", + "3500000000000000000000", + "70700000000000000000000", + "700000000000000000000", + "700000000000000000000", + "700000000000000000000", + "3500000000000000000000", + "3500000000000000000000", + "28098126416291600000000", + "3500000000000000000000", + "700000000000000000000", + "12250000000000000000000", + "38698778235304100000000", + "700000000000000000000", + "24999800000000000000000", + "77499800000000000000000", + "7000000000000000000000", + "331027023455753000000000", + "700000000000000000000", + "3441322013754190000000000", + "24999800000000000000000", + "700000000000000000000", + "282563658805107000000000", + "700000000000000000000", + "700000000000000000000", + "438200000000000000000000", + "3500000000000000000000", + "7000000000000000000000", + "700000000000000000000", + "31999800000000000000000", + "700000000000000000000", + "7000000000000000000000", + "7700000000000000000000", + "35000000000000000000000", + "4900000000000000000000", + "700000000000000000000", + "700000000000000000000", + "56999600000000000000000", + "700000000000000000000", + "10602485665800000000", + "2513308256126880000000000", + "2076494452153400000000", + "316530397397200", + "26190296843245600000000", + "1906149412229860000000", + "79876380705063700000000", + "260934453702493000000000", + "68933106279590100000000", + "29487166275538500000000", + "161245666330240000000000", + "18840617028126600000000", + "113100133618500000000000", + "91513959963447400000000", + "94685905467844900000000", + "1590375451308800000000", + "1661134296992660000000000", + "49725625571970900000000", + "2145135710611280000000", + "35123622593951300000000", + "8578381843859420000000", + "2275980528732120000000", + "10403491363462100000", + "16686996811056400000000", + "12672529542113000000000", + "27251318918576300000000000", + "502954593146370000000", + "10857607824309700000000", + "3750373554422920000000", + "13176166038595100000000", + "33712196075675700000000", + "425732796288115000000000", + "680431094033721000000", + "17932576378856000000000", + "124290376395421000000000", + "1247285812332510000000000", + "99417919518572700000000", + "203820306954279000000", + "5191547821152380000000", + "1915765998377550000000", + "91804687990722400000000", + "7401598730507370000000", + "189958632420859000000000", + "799399799598261000000", + "656644721180039000000000", + "939156864774516000000", + "18091558611882900000000", + "18442529688075700000000", + "3454828367324260000000", + "50817677020074500000000", + "2677631265999160000000000", + "474918798654353000000000", + "18361508301700700000000", + "10569012153755200000000", + "12525748785796700000000", + "61238227689914300000000", + "1485223775584830000000", + "41615617300671800000000", + "497225154003423000000000", + "726123903550606000000", + "232724281550179000000000", + "9023379659335410000000", + "2785205436063240000000", + "2280353692899730000000", + "98248528522581600000000", + "33765705341224100000000", + "5594572764722470000000", + "6431450317725490000000", + "788865388170811000000", + "36595439061748500000000", + "35542793994174300000000", + "56741584537930000000000", + "989000931209480000000", + "534366721513041000000", + "462818392845554000000000", + "838320088168542000000000", + "41302593865209600000000", + "4923400486461240000000", + "51469615634434400000000", + "3558356529482180000000", + "23324153840095800000000", + "120777382121351000000", + "41238442058216600000000", + "199869328312878000000", + "118498369206000000000000", + "18018895435485700000000", + "861530379886929000", + "11693881280643000000000", + "73329144584281600000000", + "50568294113513500000000", + "45545736375416000000000", + "181698909615612000000000", + "5735580498348410000000", + "58522100492148700000000", + "25062255755700000000000", + "189001942601394000000000", + "20276660219257000000000", + "21516757418197000000000", + "224686935012569000000000", + "696316866836138000000000", + "4168563029289580000000", + "5613080646600000000000", + "1939268824151900000000", + "6125432000199170000000", + "3628060891875000000000000", + "4587515163153880000000000", + "282841242906856000000000", + "321188331798819000000", + "89301806769450400000000", + "3815705450175260000000", + "19385295508025800000000", + "1530490881737310000000", + "250970406239765000000", + "586969776996768000000000", + "4817104553381840000000", + "22369245371495900000000", + "4882773486907200000000", + "92894842122204400000000", + "366646159906488000000000", + "18650933713748700000000", + "10062991468376900000000", + "67186878315993800000000", + "2683045248180470000000", + "6701633275329690000000", + "214560836650147000000", + "5765439823852850000000", + "17835419656239700000000", + "99585879846933700000000", + "16030916983366000000000", + "2683000703227400000000", + "11267143896183100000000", + "12182336328712700000000", + "868008146837614000000000", + "14425510705819500000000", + "7251475167310520000000", + "139348527974419000000000", + "276469909500170000000", + "262251850968558000000000", + "9273068414413280000000", + "2583100386752820000000", + "11171907846225000000000", + "97309325605096000000000", + "275621478660415000000000", + "18512937102426400000000", + "1699152113577330000000", + "14107755825235700000000", + "142052051646188000000000", + "3849062032671880000000", + "8089315025900530000000", + "70709209729438100000000", + "52291596102134300000000", + "150534636946320000000000", + "2049134102901940000000", + "9159239661691620000000", + "93342772393302600000000", + "95235059617179600000000", + "14266162256970300000000", + "20091964060643300000000", + "3334827738304940000000", + "54820002947182600000000", + "2974847393597380000000", + "2181156174370190000000", + "8306502164778580000000", + "67614797897081600000000", + "1244851039225290000000000", + "575175324616419000000", + "63247897000361400000000", + "82342167871210300000000", + "24890966651349300000000", + "180868429148327000000000", + "174230025208806000000000", + "8665122206282830000000", + "47230768421157200000000", + "4640232073794480000000", + "243542559211004000000000", + "1866933021434270000000", + "47576346391581600000000", + "10032630514761100000000", + "551699152900000000000", + "647437628070758000000000", + "8786666325069280000000", + "2712713337957580000000", + "135543136015007000000000", + "299242003202194000000000", + "15876456448252100000000", + "1987386311548830000000", + "93551344110000000000000", + "43927844405166100000000", + "430567642292964000000", + "41253564968615000000000", + "62435302866994700000000", + "38396514295188100000000", + "4241088232959340000000", + "3064438709745860000000", + "120070784230829000000000", + "427083920060770000000", + "567544820934000000000000", + "4629686143798690000000000", + "1501429162236390000000", + "22727957368226200000000", + "16807914065802500000000", + "13248222144824900000000", + "110645137542310000000000", + "336579867644273000000000", + "6213559786128670000000", + "72712313393497800000000", + "30183487811052200000000", + "13379079542521000000000", + "354264735982082000000000", + "189040818139491000", + "79622565502832000000000", + "2909402099164420000000000", + "436535287368122000000", + "1158568221090000000000", + "13994971478188900000000", + "2762625840152820000000", + "4965292376100000000000", + "857378547531889000000", + "226665910497828000000000", + "151770625676190000000000", + "74092336960358200000000", + "81549692447463500000000", + "122268835855220000000000", + "767268775591811000000000", + "1697610675067840000000", + "25500395642280400000000", + "5516991529000000000", + "59375060732274000000000", + "8731458783600000000000", + "674866421614604000000000", + "623200758989813000000000", + "872893930745320000000", + "112527329541203000000000", + "5218243034178980000000", + "153028789969137000000000", + "23886036047548300000", + "3360233647776120000000", + "17389658160308800000000", + "524875370516281000000", + "73763989615444500000000", + "40634798904379300000000", + "4255311026900600000000", + "683309704436992000000", + "525829755165305000000000", + "365281825563161000000000", + "225092809229758000000000", + "1197565806739770000000", + "1094862410343480000000", + "118656768591027000000000", + "692690184173425000000000", + "14114973868887700000000", + "289587644026561000000000", + "126576042248673000000000", + "513999515673778000000000", + "9215333791211420000000", + "177733844538762000000", + "11591530259532900000000", + "147803538326405000000", + "51830254294903700000000", + "709633092784286000000000", + "31216560743583900000000", + "464045686332740000000", + "50008923039811900000000", + "8223437992145110000000", + "77467056736236900000000", + "9678177198786450000000", + "147588013048442000000000", + "173585420351260000000000", + "27741200261187100000000", + "4485461892041390000000", + "82554674162924200000000", + "2240293400748590000000000", + "52711726085449700000", + "205010387686959000000000", + "131906804521306000000000", + "87330021557646900000", + "95649434471199700000000", + "75581207624785500000000", + "3282493148280520000000", + "212084325928238000000000", + "4413593223200000000000", + "15348751087968800000000", + "58476852905876400000000", + "8574693956652880000000", + "29750376820132500000000", + "6283896919871620000000", + "62557806498236500000000", + "871520509864269000000", + "6602315280662690000000", + "436318663517496000000", + "46781279652054600000000", + "3010413407466750000000000", + "1611304315837500000000", + "36686566471121700000000", + "12943219297299200000000", + "10126327611648900000000", + "649709048630229000000", + "10904169762141400000000", + "9071072232969590000000", + "5964319070114180000000", + "96197074328258700000000", + "17989302669366400000000", + "119410088509766000000000", + "82162015847396900000000", + "745699775464738000000000", + "621276952450972000000", + "399258383099847000000", + "33248248720684300000000", + "63757361830922500000000", + "3483725003593700000000", + "29681420831261600000000", + "1867432787334870000000", + "5336269249337900000000", + "55089072045219900000000", + "724046427866970000000000", + "1050972850795830000000000", + "1991065750547680000000", + "95219158090209200000000", + "560971621803620000000", + "3382464548522960000000", + "6237739862616100000000", + "13997528659387100000000", + "1271244273955960000000000", + "6202234905032390000000", + "56533058147722100000000", + "111056930117748000000000", + "20241476527335100000000", + "13359579297670800000000", + "244700899092545000000000", + "21308537281240500000000", + "12090613000313000000000", + "1172068573646600000000", + "10703653231714000000000", + "55575822529979000000000", + "1980564362708480000000", + "7317160501695160000000", + "20227317726314600000000", + "89469155203863600000", + "13720893192631300000000", + "19026196761469100000000", + "1932662793532890000000", + "1114626828757660000000", + "93706112341160700000000", + "7258008897629410000000", + "15447576281200000000000", + "13484878430270300000", + "3407075238484640000000000", + "31334630701722900000000", + "715717819978378000000", + "2134219401197360000000", + "786573524228323000000", + "6227034515573540000000", + "67424392151351400000000", + "6658869261752680000000", + "413064291555381000000000", + "5759739156276000000000", + "4301182051405710000000", + "601675733095054000000000", + "2238110431808110000000", + "237317491139830000000000", + "191278861162104000000000", + "62367562740000000000", + "473372782313472000000000", + "2430384261502710000000", + "23863111755700700000000", + "152552075991877000000000", + "40076584307330900000000", + "583664555460000000000000", + "333324952483953000000", + "33249493167930000000000", + "449508744821480000000000", + "7496096215900450000000", + "3962056266878050000000", + "7711103696993470000000", + "459300928906112000000000", + "57367761437442100000000", + "10761297176744600000000", + "246815719201231000000000", + "52549734378127800000000", + "224668754461738000000000", + "9527138103705070000000", + "7693055835617630000000", + "2725708179039980000000", + "21262110630747100000000", + "4679139621075990000000", + "3873004793541700000000", + "68437218614349800000000", + "8043874342621060000000", + "4636152221990680000000", + "683717965791367000000", + "334407398491810000000", + "37268456888839100000000", + "9932915823009380000000", + "24457226639740700000000", + "1618943456370440000000000", + "22648090163373500000000", + "4269934428752100000000", + "17711011865310600000000", + "1527050294915700000000000", + "40828309333125800000000", + "56958399714215400000000", + "3766166944302650000000", + "292471746502122000000000", + "6235402297341530000000", + "5584649470856220000000", + "2028860796954020000000", + "180420962352139000000000", + "2909402099164420000000000", + "18168698391743700000000", + "40529569737955000000000", + "3060352798670210000000", + "37743394689708900000000", + "94519726245531000000000", + "13134889899603500000000", + "7657005540706020000000", + "40458906900611500000000", + "3316637015206150000000", + "1400162681606850000000", + "613489458024800000000", + "20748302140643900000000", + "2890290787317720000000", + "8861801571017660000000", + "565699216017207000000", + "24146417262540800000000", + "1243797802439850000000", + "33890998433836900000000", + "241239562971118000000000", + "142788118763215000000000", + "8253212337835790000000", + "45019594193004400000000", + "55087034315589200000000", + "32678929338330500000000", + "73296571200977100000000", + "337121961632892000000000", + "9508445473348790000000", + "11449700986064300000000", + "1494315028945280000000", + "34294254265343800000000", + "67402262091869200000000", + "18673284224398500000", + "139999348243681000000", + "13082477238239500000000", + "6677175238744580000000", + "647925711065999000000", + "28173702954232800000000", + "805288269736064000000000", + "27574842047325200000000", + "84699739411609700000000", + "871138201533291000000000", + "855235980495858000000000", + "6758690237225820000000", + "2132510610658660000000", + "6409904882159680000000", + "47706376969198000000000", + "17088746616896800000000", + "9283745059210230000000000", + "24577598495513400000000", + "40202561246068400000000", + "10867989716112600000000", + "53959942136954100000000", + "55169915290000000000000", + "82085453198239300000000", + "29752318498935300000000", + "5516991529000000000000", + "14177960459979000000000", + "92738211802076600000000", + "77088072125010300000000", + "4595178863351950000000", + "3389793521582190000000", + "4760292915833340000000", + "15980201917275600000000", + "4609365819012110000000", + "5700073658677300000000", + "7986679305196730000000", + "412573818588267000000", + "34633772175548800000000", + "3907878384912320000000000", + "31132043868496300000000", + "11423714762203500000000", + "90404501125009400000000", + "15015222843787300000000", + "16764752912776000000000", + "9732979549496000000000", + "87393343794551000000000", + "328314505789069000000", + "62367562740000000000000", + "1718985616188750000000000", + "5460046094798400000000", + "3603793106619780000000", + "2159077793577170000000", + "26097249750470200000000", + "12015265365723600000000", + "532849207640182000000", + "40678788853989400000000", + "5335775079605430000000", + "41462108300513800000000", + "1393254454549020000000000", + "11618746778510600000000", + "18467065069811200000000", + "40535739239168500000000", + "4145732130925890000000", + "11718234684533300000000", + "132144599756815000000000", + "551699188183930000000", + "43189689204002200000000", + "47201000208634300000000", + "477543412477717000000000", + "146001449155250000000000", + "26714715143082900000000", + "14146832146360200000000", + "2386460047785010000000000", + "36960000000000000000000000", + "223065763551343000000000", + "275849576450000000000000", + "118283810654331000000000", + "24510070218194500000000", + "311837813700000000000000", + "3945144641889750000000", + "1781842931547050000000", + "4711792235347860000000", + "497249361000000000000000", + "1300015995345370000000000", + "695730310208492000000", + "414218733301934000000000", + "106162039948469000000000", + "231713644218000000000000", + "155778727441359000000000", + "9750476154452590000000", + "885349637532426000000", + "22116511541065300000000", + "537167877373048000000", + "104366566517494000000000", + "17723329336693700000000", + "928746209917108000000000", + "1344459099788760000000000", + "2909402099164420000000000", + "46860690151916100000000", + "805031767673847000000000", + "86570779540312200000000", + "7728375931562630000000", + "2483089998460740000000", + "2089704255183530000000", + "8680485128464710000000", + "19626288016197800000000", + "1655059432151480000000", + "109161925533307000000000", + "6771410756017490000000", + "6747914575116700000000", + "41178121085419500000000", + "4655362664294900000000", + "472635698997054000", + "114643170397382000000000", + "10267365269000000000000000", + "8852793230763510000000", + "4404188788835030000000000", + "71720889877000000000000", + "4509951345441450000000", + "5936520375832220000000", + "37349496000634100000000", + "427836876660979000000000", + "6302130727043660000000", + "1241067931330440000000", + "982982538857953000000", + "5647349772105750000000", + "60632360308551800000", + "74334591334499300000000", + "7796466187674060000000", + "95452925481154800000", + "22860265809159500000000", + "574420029339790000000", + "445113781551096000000000", + "177440486313685000000000", + "1552460646779970000000", + "50408487116633800000000", + "39763068994925500000000", + "14025211385968300000000", + "2909402099164420000000000", + "766520138646937000000", + "29795536780931000000000", + "345603372059020000000000", + "868422904191896000000", + "575814141931471000000000", + "3371219607567570000000", + "48083095825158200000000", + "29898938166674500000000", + "15425704969623800000000", + "46806646245060900000000", + "10884459496643900000000", + "20611049018054100000000", + "19179823892471600000000", + "17639989892724900000000", + "1106203630048560000000", + "41806840332245700000000", + "2358367707157810000000000", + "109177143562739000000000", + "1459899806234540000000", + "3742053764400000000000", + "172407270353484000000000", + "181925888793922000000000", + "59547401319730400000000", + "67495373958634400000000", + "52804917873774900000000", + "1213639058724320000000", + "30343475075277400000000", + "785163046948008000000", + "8285886952310630000000", + "1726063321587050000000", + "56538316120943200", + "109834243331086000000000", + "445499573633412000000000", + "19309470351500000000000", + "53516443489640600000000", + "18191421309341000000000", + "52687053013748200000", + "10927155049242900000000", + "958667216652177000000", + "11229847434150800000000", + "58958207760280200000000", + "90966776763469400000", + "168872311661191000000000", + "10766285141530500000000", + "7079316475236830000000", + "6248635277046640000000", + "74094636632837400000000", + "5965393163047930000000", + "9573823775045300000000", + "1149457875219370000000", + "27584957645000000000000", + "6346816423435040000000", + "279999999697448000000000", + "216311546925898000000000", + "3450841555014860000000", + "97443696296615500000000", + "3365246876970250000000", + "6367067079347330000", + "2874081232258060000", + "25628929349410000000000", + "124054114943046000000000", + "20294576261006200000000", + "494123055049631000000000", + "7079561175891890000000", + "16893949873324300000000", + "8404486757172640000000", + "63324542632398200000000", + "404546352908108000000", + "71948876876004900000000", + "198758124444544000000000", + "2909402099164420000000000", + "8829475290051100000", + "362840753042739000000", + "9896487157363650000000", + "133769187659231000000000", + "27157498752969200000000", + "11628523031871100000000", + "2760036019587230000000000", + "39214410314612400000000", + "2809113112498940000000", + "35118650089838200000000", + "11847448988134600000000", + "337121960756757000000", + "133465040612635000000", + "33880225382928400000000", + "1516613549471860000000", + "17590720808092200000000", + "3838657796592000000000", + "623675627400000000000000", + "443800257480574000000", + "195345411731964000000000", + "1489289568147310000000", + "57277145397850400000000", + "2909402099164420000000000", + "4238195636619460000000", + "7066395121143970000000", + "693300680726532000000", + "114418266099801000000000", + "1379247882250000000000", + "307955409215662000000000", + "1641881960897060000000", + "5590676321047540000000", + "82969291005597900000000", + "31824786145484300000000", + "27095108594235200000000", + "572995205396250000000000", + "221417613944964000000000", + "52854397528472100000000", + "59725526390478700000000", + "10821810445189800000000", + "5504368172469050000000", + "76743755439844100000000", + "46379507913656500000000", + "29380826968074400000000", + "10910420738298800000000", + "110284363758587000000000", + "16769461505475200000000", + "21081086244974800000000", + "25820186581658100000000", + "40671542441210800000000", + "992120897165407000000000", + "10672989700775100000000", + "678451993050976000000", + "5021765620317800000000", + "11226161293200000000000", + "29821575832432400000", + "34911148484410300000000", + "13963868510620700000000", + "2267365382999500000000", + "157096755808780000000000", + "56384777367857500000000", + "2897942078968040000000", + "139880281183807000000000", + "8686064400485200000000", + "164496488537501000000", + "3197537446589480000000", + "41609166948708700000", + "371686097382089000000000", + "117972266877242000000000", + "26464199440915000000000", + "273669690263550000000", + "8621056337215880000000", + "3168238195527900000000", + "171614985779596000000000", + "20100573451382800000000", + "24513933834222500000000", + "27781032225739300000000", + "23143988359173400000000", + "1314530300251970000000000", + "3713826227325340000000", + "331451379576926000000000", + "5045812374935730000000", + "11033983058000000000000", + "107503387238580000000000", + "5516991529000000000000", + "36275830366292800000", + "623675627400000000000", + "623675627400000000000", + "4009580937701170000000", + "3371219607567570000", + "69767014448870500000000", + "733472616336446000000000", + "115400699114051000000000", + "4719707450594590000000", + "43753003738671800000000", + "158125486018951000000000", + "24988144893759100000000", + "21900927562079400000000", + "9693162569062110000000", + "2758495764500000000000", + "30572293671637100000000", + "33853980832512000000000", + "20705179634139600000000", + "7921118064188510000000", + "7247259019346760000000", + "43764794410311600000", + "143741207033173000000000", + "6339267167826990000000", + "12903946187208100000000", + "62820404900654900000000", + "3009338650153920000000", + "60701162352704100000000", + "477585243602979000000000", + "11361492569798100000000", + "63511906375011100000000", + "2568645832740220000000", + "41171818556957100000", + "26963447923271100000000", + "1216330969830820000000", + "84492535034905000000000", + "2750992456034050000000", + "47567909826813400000000", + "69763795277889100000000", + "20382528641986400000000", + "128047223560434000", + "1247351254800000000000000", + "56773536766884300000000", + "2775789416304410000000", + "22730626677199200000000", + "15762547989779700000000", + "415167366214257000000", + "530235470031585000000000", + "18774359895389100000000", + "47088819587517500", + "42885127545212000000000", + "74163268067312100000", + "21284366066595900000000", + "2237473003613420000000", + "12096962142869300000000", + "2206796994903020000000", + "9934186523353710000000", + "62962410996398600000000", + "9339485898073720000000", + "13393278133411900000000", + "361352522085377000000000", + "30339948396869700000000", + "4143478737845200000000", + "177036355080736000000000", + "23663914854659800000000", + "112192991968260000000000", + "11442932212307900000000", + "108773147542576000000000", + "13224284539082200000000", + "50657868716836000000000", + "23771902874190100000000", + "135817219471783000000000", + "57742073584300900000000", + "34428508851978600000000", + "9899448075676560000000", + "62498919561229700000000", + "27989850627387800000000", + "87780478199674400000000", + "8218627115030320000000", + "118631599727074000000000", + "24088642601929700000000", + "13086983515581600000000", + "1007150569954920000000", + "27217828055363400000000", + "2430109130535250000000", + "880988610495138000000", + "25089580990323000000000", + "18538093043059700000000", + "67221528842072000000000", + "5877520060747220000000", + "2719727715917840000000", + "8358774383481750000000", + "6950390109535120000000", + "7566725060042630000000", + "28314442914342700000000", + "20627730385271500000000", + "62370246764068100000000", + "36445873390924100000", + "206810838045840000000000", + "1042031103210730000000", + "2334790815072800000000", + "30089325503653400000000", + "351476091044834000000", + "17616412904992400000000", + "5035103173238540000", + "15397326392706900000000", + "10482158025860600000000", + "4812527162714520000000", + "3456047885170630000000", + "2166048860059500000000", + "537589152403100000000", + "1801573361400320000000", + "21156650317724300000000", + "32456178538701500000000", + "33873569923653500000000", + "2598268918741230000000000", + "278744597268628000000000", + "148866467086687000000000", + "33209161867610300000000", + "2270473595840660000000", + "10529286989025500000000", + "28389561591464300000000", + "23851536551670100000000", + "24007187554913100000000", + "57746166854367800000000", + "63620463595498500000000", + "5151166185422080000000", + "2904936812607850000000", + "74301299340506800000000", + "32167028030181100000000", + "4415865405453250000000", + "12798243048915500000000", + "4193532551074860000000", + "107370762436332000000000", + "1257441999169940000000", + "2531880102781820000000", + "244288974700026000000000", + "168030694870841000000000", + "3008290099693690000000000", + "45652039021281900000000", + "103665915936931000000000", + "441359322320000000000000", + "3164396764081920000000", + "12593159980195300000000", + "23706576961575300000000", + "4093288417193520000000", + "182877076123405000000000", + "25970759532457600000000", + "2709507925774800000000", + "6236928262017940000000", + "3566619535925950000000000", + "44602631707175200000000", + "2478684148059030000000000", + "5805699003800880000000", + "6620389834800000000000", + "1065369126151080000000", + "2955587703951390000000", + "21546982669019200000000", + "66431647602237500000000", + "56624150366404000000000", + "17631549019466700000000", + "11142569962465400000000", + "66277048896055500000000", + "49193609366080100000000", + "5922153908104060000000000", + "7530230794001360000000", + "1427271995945020000000", + "37633754933653600000000", + "4869602518408980000000", + "1905744860928130000000", + "2689900009085770000000", + "156713402586954000000000", + "4263263809122940000000", + "26048456354742000000000", + "1826933268278630000000", + "20800499749141400000000", + "263281414414537000000", + "1594128471902180000000", + "103909360425662000000000", + "2453695529650880000000", + "59029517804540300000000", + "6399710173640000000000", + "63911969840666000000000", + "15878429862297600000000", + "18530798502515300000000", + "420567988043811000000", + "189106287284016000000000", + "33336687944000000000000000", + "13815729863386800000000", + "3406945189686620000000", + "59173270305729600000000", + "149107879162162000000000", + "153862511349028000000000", + "2617952510110660000000000", + "3558625045950800", + "242562290147757000000000", + "66622443095591000000000", + "101639356292888000000000", + "54746875478979400000000", + "44039131016679300000000", + "64615053289051300000000", + "11991110936198700000000", + "4881377695642260000000", + "137639145073416000000000", + "87440305027490200000000", + "3081045980486930000000", + "1226438967148610000000000", + "1329091785462440000000", + "900465634146997000000000", + "4988294448287820000000", + "619828448148000000000000", + "46893270767860800000000", + "5283851414926280000000000", + "5352597742770740000000", + "996666715837819000000000", + "133121249255903000000", + "83358408212972900000000", + "298457711206604000000000", + "8631621928160100000000", + "7663183199046840000000", + "18403007455391700000000", + "16449957088669200000000", + "56301749276264500000000", + "5852812942060680000000", + "88191095511788700000000", + "25693172931691600000000", + "32978534287337900000000", + "14968215057600000000000", + "11083970096246600000000", + "34659805511953500000000", + "110317801534869000000000", + "107737719409414000000000", + "42508492966918100000000", + "108196425586330000000000", + "2033756172292920000000", + "5190913829028880000000000", + "20949744620196400000000", + "137046078112563000000000", + "30773222452559000000000", + "238042931908904000000000", + "62735447275653600000000", + "8616444660294470000000", + "4930973932668980000000", + "55169915290000000000000", + "1306732922840100000000000", + "23835728528604600000000", + "2560936861229880000000", + "6669120764162440000000", + "2228611303169700", + "110782230700061000000000", + "2405601971892220000000", + "3785674422987690000000", + "8305806296609820000000", + "4597801668450600000000", + "6294087852604680000000", + "711933003774673000000000", + "43906552380097700000000", + "917497012488163000000", + "1703001034876280000000", + "234113887712637000000000", + "195892582467667000000000", + "1248606083393260000000", + "50705339496860800000000", + "385291613852874000000", + "238986748003075000000000", + "842804901891892000000000", + "111009462239370000000000", + "10654646923203900000000", + "76743755439844100000000", + "103202771208286000000000", + "5370277944628170000000", + "5526395946610910000000", + "24304101265732100000000", + "204891956655247000000000", + "9355134411000000000000", + "4509341965208960000000", + "7437206240716510000000", + "30102468373021800000000", + "4706404841219600000000", + "924045763881019000000000", + "2042959082185950000000", + "3756588175861420000000", + "33900249802715400000000", + "14471163287713900000000", + "19217317107076200000000", + "217645315819050000000000", + "11872919270263600000000", + "17831996000000000000000", + "236745570164928000000000", + "16668101089553200000000", + "108099408931003000000000", + "11831827950368600000000", + "376283677271250000000000", + "35360793253669400000000", + "41397906368637400000000", + "92385429654756200000000", + "3652303362764770000000", + "225604865585414000000000", + "60229200324076400000000", + "264681186490461000000", + "439522164140843000000000", + "9149475147496690000000", + "45136333108904900000000", + "190201980837499000000000", + "254584453923253000000", + "33744584517779600000000", + "9816100994581990000000", + "57903949218111900000000", + "4855495152873640000000", + "3322179143610120000000", + "11644764491837400000000", + "15175051724273800000000", + "15086586308956200000000", + "6276963414379310000000", + "123807374310284000000000", + "19832069695685500000000", + "2685487848666520000000", + "56909285632588000000000", + "76542949114228800000000", + "1124372328441090000000", + "1127039169205910000000", + "41020528194136100000000", + "1481412296011410000000", + "11800539239200300000000", + "1662132116583680000000", + "568428604967443000000000", + "19845142836209100000000", + "5441039668007030000000", + "942057930048386000000", + "42835777950124000000000", + "4850615368811720000000", + "2949142508153760000000", + "874404022888118000000000", + "150431366773843000000", + "73682183413849700000000", + "288837525341637000000000", + "2884923644033240000000", + "4413593223200000000000", + "2485705731978240000000", + "17600787286968600000000", + "11394661581846700000000", + "35198330680493300000000", + "185234251688991000000000", + "402477245215322000000000", + "12093247272125100000000", + "12550548193135000000000", + "14564623548612500000000", + "36358271148613500000000", + "103916423808888000000000", + "7080713591296290000000", + "551699152900000000000", + "827548729350000000000", + "7851935249163770000000", + "140352929892287000000000", + "542335112537305000000000", + "247135546239874000000000", + "161122799773700000000000", + "192399371921233000000000", + "32586671209230800000000", + "57978897639680500000000", + "2945589355194260000000", + "1103398305800000000000", + "28546493350814100000000", + "1177709085530680000000000", + "58017875634191700000000", + "10614825227652900000000", + "175173431817848000000", + "38469952873549600000000", + "15613421160060900000000", + "63252867348451600000000", + "1248578289847130000000", + "439429719099919000000", + "458717861762611000000", + "1666828635550010000000", + "1193166171524210000000", + "6827292171047310000000", + "36459529393105000000000", + "3357655400176220000000", + "20617989420098800000000", + "46604768740956200000000", + "11595728395592800000000", + "576432043350241000000", + "498162771815240000000", + "21204971331600000000000", + "604945583657267000000", + "1684666010080800000000", + "7353578009524390000000", + "8762155339739080000000", + "168900880485534000000000", + "335961632636961000000", + "623675627400000000000", + "162392775001216000000000", + "1012284725145800000000", + "78578607059630300000000", + "21320545270648700000000", + "2758309070230680000000", + "4359145017714670000000", + "105856431752225000000", + "102181941847258000000000", + "233996243923637000000000", + "35878952366227300000000", + "41377436467500000000", + "223140393076926000000000", + "11270118853964700000000", + "11595543299043700000000", + "62642509572441500000000", + "20328692385145800000000", + "2319619466396040000000", + "823843425658521000000000", + "31556394306966100000000", + "13796529756035000000000", + "1851994466344500000000", + "441009645611431000000000", + "108110716599716000000000", + "578033831580453000000000", + "6470842294967830000000", + "63164899779223400000000", + "50347989115403700000000", + "10851908657543200000000", + "7324318513800240000000000", + "12137381363800000000000", + "10507124271784400000000", + "252984577171841000000", + "6750117813773950000000", + "5081544105840680000000000", + "146552123729600000000000", + "108078781004553000000000", + "3160135289112300000000", + "20330597187170400000000", + "317378523451694000000000", + "19030391638045500000000", + "566193950263240000000000", + "164658078624408000000000", + "18923921389153700000000", + "15310781442196600000000", + "1073495212719060000000", + "40911612366273700000000", + "177243033492143000000000", + "88384916250289100000000", + "444699737307917000000", + "326939410885287000000", + "11166855323726400000000", + "756879199814000000000000", + "551975002476450000000000", + "29821575832432400000", + "118928921343591000000000", + "27825128297290500000000", + "10703159788798300000000", + "2824247635485830000000", + "784563374179046000000000", + "2055336969614260000000000", + "127586032913057000000000", + "649540209399021000", + "34113165830185200000000", + "116743639468912000000000", + "3758292123854260000000", + "1010757784722070000000000", + "496529237610000000000", + "2256250830244480000000", + "8631647255920980000000", + "19868241143392600000000", + "9944912395284720000", + "21859829044974900000000", + "1221984416224330000000000", + "502645201090091000000000", + "579968268047887000000000", + "62215154972109700000000", + "753391214112001000000", + "653785161726325000000", + "14153570020634400000000000", + "862654894866303000000000", + "14641417807794300000000", + "4642552777951680000000", + "605566703787214000000000", + "67786550452785500000000", + "499585386803338000000000", + "33548138831219600000000", + "2727048912784700000000", + "557856241008564000000", + "26997393461831500000000", + "1236555222253430000000", + "77685948751583400000000", + "45135889572286000000000", + "10852772252454200000", + "5332067332530980000000", + "1207557564293390000000", + "208445391142378000000000", + "87546093709663500000000", + "6417358094545710000000", + "18002325707379100000000", + "815762452681420000000", + "58582844363693900000000", + "8933948820673910000000", + "1139740245004380000000", + "1321435549416140000000", + "47611604406505000000000", + "694862327880939000000", + "1396077042432270000000", + "10114759733694000000000", + "8003218607368190000000", + "700833119289612000000000", + "6177583703817700000000", + "213878988826899000000000", + "463089461141888000000000", + "10540118937269400000000000", + "599410871068563000000000", + "52002041917772900000000", + "8946366845719560000000", + "2758495764500000000000", + "4085083236793990000000000", + "2093973607745550000000000", + "3636894368310540000000000", + "13126384251428100000000", + "5826851346679480000000", + "238204349493831000000000", + "766342772799194000000", + "7521779433770290000000", + "5717727865576500000000", + "4569872748776970000000", + "3174350247238620000000", + "20554178509366500000000", + "27050666131122200000000", + "2544671673136560000000", + "9980331715136730000000", + "1933806067537780000000", + "1031686164147290000000", + "8399869738398540000000", + "5014880734125350000000", + "4292161788683470000000", + "2178181479510010000000", + "4474282702186100000000", + "8818517383907610000000", + "336204948097149000000000", + "10596543793083700000000", + "57283170957287000000", + "497078663329668000000", + "104977424661145000000000", + "4155455861340040000000", + "2057611795499480000000", + "23009934010461400000000", + "11608931927484600000000", + "3476958916542650000000", + "37990338066825600000000", + "7024701154571790000000", + "890001976397838000000", + "343619955210112000000", + "782899706560652000000000", + "1933297571040840000", + "395066049686813000000000", + "61250360574331700000", + "172954072916277000000000", + "20488947641474300000000", + "7196420051622580000000", + "244839525922229000000000", + "116998114719940000000", + "3302034629882290000000", + "2083498339228890000000", + "421478143541406000000", + "6332461880828440000000", + "41377436467500000000000", + "4748462230381290000000000", + "138051687889640000000000", + "33254469976715200000000", + "25322991118110000000000", + "246686460716218000000000", + "10075310500300000000000", + "42308835564903500000000", + "64672302116482000000000", + "134570364871332000000000", + "101748198089111000000000", + "10357888324877100000000", + "31727621966215800000000", + "24835598767229900000000", + "59954640388711000000000", + "59782402187610800000000", + "160672704290683000000000", + "102925909648829000000", + "125390793645290000000000", + "98682780240710400000000", + "4549022714839540000000", + "135307625471539000000000", + "30284073672737400000000", + "22474568391687300000000", + "47258709202126200000000", + "8979599636660670000000", + "20244589158231800000000", + "8670484616998950000000", + "74362357972469400000000", + "6080239250030900000000", + "170607045936850000000000", + "3750606761140710000000", + "1123982045720600000000", + "26485198856737900000000", + "8063178679564190000000", + "4483464851212950000000", + "108906684858919000000000", + "1869503490622440000000000", + "5515801142528230000000", + "35621236542900500000000", + "635095463962877000000", + "237777577292209000000000", + "38607351902104600000000", + "204466313757282000000", + "19508225646242600000000", + "11093280720573500000000", + "8239517774300360000000", + "118974145929408000000000", + "7668836838172590000000", + "87306471808830700000", + "6163161100109590000000", + "41677048966090600000000", + "19001970790529000000000", + "1446575346537260000000", + "3779657547173460000000", + "551699152900000000000", + "670486333472340000000000", + "4178327815431760000000", + "20497093575018400000000", + "1677596710740750000000", + "1737279587294630000000", + "6525229076767460000000", + "44448730144654500000000", + "269594740353681000000", + "827548729350000000000", + "41309904396315200000000", + "108792549066083000000000", + "29821575832432400000", + "11530919105737300000000", + "4013884798687270000000", + "346874741542099000000000", + "152134916100106000000000", + "5479356497167540000000", + "2307599821380000000000", + "92099048516369000000000", + "549183566011821000000000", + "5556913405496800000000", + "131918420852024000000000", + "140290778350807000000000", + "13795418145507600000000", + "3628060891875000000000000", + "1346393890771150000000000", + "594523498180657000000000", + "10828231442738700000000", + "18867549836601800000000", + "688785864746820000000", + "2439018924335660000000000", + "2909402099164420000000000", + "51093938054441600000", + "102572838685266000000000", + "31588176163169900000000", + "503416719374400000000000", + "17428418605156900000000", + "5758061256154080000000", + "116950944093890000000000", + "40281910132973800000000", + "69248118232244800000000", + "54065914977680700000000", + "325795610811556000000000", + "1391680038032650000000", + "748410752880000000000", + "3107774810270030000000", + "5170797490397870000000", + "19977562088223700000000", + "372337653222345000000000", + "134046375699461000000000", + "63190450337686900000000", + "1808659319460000000000", + "7038058586046910000000", + "3414699755345980000000", + "108140990718061000000000", + "6500758775779740000000", + "44351508366021500000000", + "8749571843959080000000", + "255147848852009000000", + "3167378718673160000000", + "97013802876916700000000", + "11543880254463600000000", + "5000748842218520000000000", + "14211573538965400000000", + "34149769390436600000000", + "60611885878321700000000", + "21687180658049300000000", + "3184863620497910000000", + "217411492269705000000000", + "36084860024793900000000", + "7274184092902040000000000", + "2201122552892460000000000", + "854442954503779000000000", + "53014629182909200000000", + "438094612863659000000000", + "355282560821282000000", + "286298464815918000000000", + "74577081407035400000", + "402969609545369000000000", + "330917147101019000000000", + "43472162393445700000000", + "22469042689002100000000", + "8163489555714540000000", + "161386503498471000000000", + "116779415084789000000000", + "88589021640737300000000", + "6144481509093220000000", + "775603352121724000000000", + "3861894070300000000000", + "6875905130245960000000", + "2358725047629300000000", + "118498369206000000000000", + "5911525294404440000000", + "161118348426052000000000", + "76044716290485800000000", + "49132658386042300000000", + "74587842817507500000000", + "6733936991088720000" + ] +} \ No newline at end of file