From 2a0bc490de2ef9984803a1fdcae2b309195f8e11 Mon Sep 17 00:00:00 2001 From: naveed Date: Sat, 12 Apr 2025 13:20:27 +0330 Subject: [PATCH 01/48] Make deploy scripts create2 compatible --- contracts/helpers/ProxyImports.sol | 4 + contracts/helpers/create2Factory.sol | 25 +++++ tasks/symmStaking.ts | 94 +++++++++++++++--- tasks/symmVesting.ts | 139 +++++++++++++++++++++++---- 4 files changed, 230 insertions(+), 32 deletions(-) create mode 100644 contracts/helpers/ProxyImports.sol create mode 100644 contracts/helpers/create2Factory.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/tasks/symmStaking.ts b/tasks/symmStaking.ts index 5e1f834..c845fe4 100644 --- a/tasks/symmStaking.ts +++ b/tasks/symmStaking.ts @@ -1,16 +1,86 @@ -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:staking", "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, + } + }) diff --git a/tasks/symmVesting.ts b/tasks/symmVesting.ts index 4d6b705..4f71fd7 100644 --- a/tasks/symmVesting.ts +++ b/tasks/symmVesting.ts @@ -1,34 +1,133 @@ -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") + .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 - const VestingPlanOps = await ethers.getContractFactory("VestingPlanOps") - const vestingPlanOps = await VestingPlanOps.deploy() - await vestingPlanOps.waitForDeployment() + // 1. Deploy the VestingPlanOps library first + console.log("Deploying VestingPlanOps library...") + const VestingPlanOpsFactory = await ethers.getContractFactory("VestingPlanOps") - const VestingFactory = await ethers.getContractFactory("SymmVesting", { + // 2. Get an instance of your Create2Factory contract + const create2Factory = await ethers.getContractAt("Create2Factory", factory) + + // 3. Prepare library deployment bytecode + const libDeployTx = await VestingPlanOpsFactory.getDeployTransaction() + const libBytecode = libDeployTx.data + if (!libBytecode) { + throw new Error("Cannot obtain library deployment bytecode") + } + + // 4. Compute a deterministic salt for library + const librarySalt = ethers.keccak256(ethers.toUtf8Bytes(`library-vesting-planops`)) + console.log("Library salt:", librarySalt) + + // 5. Compute the predicted library address + const predictedLibAddress = await create2Factory.getFunction("getAddress")(libBytecode, librarySalt) + console.log("Predicted library address:", predictedLibAddress) + + if (!dryRun) { + // 6. Deploy the library via the factory using CREATE2 + console.log("Deploying library via CREATE2...") + const libTx = await create2Factory.deploy(libBytecode, librarySalt) + await libTx.wait() + console.log("Library deployed at:", predictedLibAddress) + } + + console.log() + + // 7. Get the contract factory for the logic contract with library linkage + const SymmVestingFactory = await ethers.getContractFactory("SymmVesting", { libraries: { - VestingPlanOps: await vestingPlanOps.getAddress(), + VestingPlanOps: predictedLibAddress, }, }) - 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 + // 8. Prepare implementation deployment bytecode + const implDeployTx = await SymmVestingFactory.getDeployTransaction() + const implBytecode = implDeployTx.data + if (!implBytecode) { + throw new Error("Cannot obtain implementation deployment bytecode") + } + + // 9. Compute a deterministic salt for implementation if not provided + const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vesting`)) + console.log("Implementation salt:", implementationSalt) + + // 10. Compute the predicted implementation address + const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt) + console.log("Predicted implementation address:", predictedImplAddress) + + if (!dryRun) { + // 11. 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() + } + + // 12. 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) + + // 13. 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") + } + + // 15. 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) + + // 16. Compute the predicted proxy address + const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue) + console.log("Predicted proxy address:", predictedProxyAddress) + + if (!dryRun) { + // 17. 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 { + library: predictedLibAddress, + implementation: predictedImplAddress, + proxy: predictedProxyAddress, + } }) From c0848e103c50b58a81e59e3710f51cacc9516991 Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Sun, 13 Apr 2025 13:25:45 +0330 Subject: [PATCH 02/48] Finish first design --- contracts/vesting/SymmVestingRequester.sol | 58 ++++++++++++++++++++++ contracts/vesting/interfaces/IVesting.sol | 33 ++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 contracts/vesting/SymmVestingRequester.sol create mode 100644 contracts/vesting/interfaces/IVesting.sol diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingRequester.sol new file mode 100644 index 0000000..4887bd6 --- /dev/null +++ b/contracts/vesting/SymmVestingRequester.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.18; + +import {IVesting} from "./interfaces/IVesting.sol"; + +contract SymmVestingRequester { + + error MismatchedArrays(); + error ZeroAmount(); + + //init + + uint256 public totalTime = 180 days; + uint256 public penaltyPerDay = 25e16; + uint256 public launchTime; + uint256 public totalRegisteredAmount = 0; + uint256 public totalRegisteredUsers = 0; + uint256 public totalVestedAmount = 0; + mapping(address=>uint256) public registeredAmounts; // user => amount + address public symmAddress; + address public symmVestingAddress; + + function registerPlans(address[] memory users, uint256[] memory amounts) external {//onlyRole + if(users.length != amounts.length) + revert MismatchedArrays(); + for(uint32 i=0; i amounts[i]) + totalRegisteredAmount -= registeredAmounts[users[i]] - amounts[i]; + else + totalRegisteredAmount += amounts[i] - registeredAmounts[users[i]]; + if(registeredAmounts[users[i]]!=0) totalRegisteredUsers += 1;//TODO: How to check it's not already added? + registeredAmounts[users[i]] = amounts[i]; + } + } + + function requestVestingPlan() external { + if(registeredAmounts[msg.sender] == 0) revert ZeroAmount(); + address[] memory users; + uint256[] memory amounts; + users[0] = msg.sender; + amounts[0] = registeredAmounts[msg.sender]; + IVesting(symmVestingAddress).setupVestingPlans( + symmAddress, + block.timestamp, + _getEndTime(), + users, + amounts + ); + totalVestedAmount += registeredAmounts[msg.sender]; + registeredAmounts[msg.sender] = 0; + } + + function _getEndTime() private view returns(uint256){ + uint256 timePassed = (block.timestamp / 1days - launchTime / 1days) * 1days; + if(timePassed > totalTime) timePassed = totalTime; + return block.timestamp + totalTime - timePassed + timePassed * penaltyPerDay / 1e18; + } +} 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); +} From 08dd9d1d8ca540d37dc32c973926c7c108531b2d Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Sun, 13 Apr 2025 15:11:11 +0330 Subject: [PATCH 03/48] Add inheritance --- contracts/vesting/SymmVestingRequester.sol | 35 ++++++++++++++++++---- tasks/symmVestingRequester.ts | 0 tests/symmVestingRequester.behavior.ts | 0 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 tasks/symmVestingRequester.ts create mode 100644 tests/symmVestingRequester.behavior.ts diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingRequester.sol index 4887bd6..9b0ee37 100644 --- a/contracts/vesting/SymmVestingRequester.sol +++ b/contracts/vesting/SymmVestingRequester.sol @@ -1,17 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.18; +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {IVesting} from "./interfaces/IVesting.sol"; -contract SymmVestingRequester { +contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable { error MismatchedArrays(); error ZeroAmount(); - //init + bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); uint256 public totalTime = 180 days; - uint256 public penaltyPerDay = 25e16; + uint256 public penaltyPerDay = 25e16; // 0.25e15 uint256 public launchTime; uint256 public totalRegisteredAmount = 0; uint256 public totalRegisteredUsers = 0; @@ -20,7 +24,20 @@ contract SymmVestingRequester { address public symmAddress; address public symmVestingAddress; - function registerPlans(address[] memory users, uint256[] memory amounts) external {//onlyRole + function initializer(address admin, address _symmAddress, address _symmVestingAddress) public initializer{ + __AccessControlEnumerable_init(); + __Pausable_init(); + + _grantRole(SETTER_ROLE, admin); + _grantRole(PAUSER_ROLE, admin); + _grantRole(UNPAUSER_ROLE, admin); + + symmAddress = _symmAddress; + symmVestingAddress = _symmVestingAddress; + launchTime = block.timestamp; + } + + function registerPlans(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { //TODO: whenNotPaused? if(users.length != amounts.length) revert MismatchedArrays(); for(uint32 i=0; i totalTime) timePassed = totalTime; diff --git a/tasks/symmVestingRequester.ts b/tasks/symmVestingRequester.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/symmVestingRequester.behavior.ts b/tests/symmVestingRequester.behavior.ts new file mode 100644 index 0000000..e69de29 From 30abbb4dfdf3dfde16f19978c3d635ba62637f32 Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 14 Apr 2025 10:21:17 +0330 Subject: [PATCH 04/48] Fix bugs --- contracts/vesting/SymmVestingRequester.sol | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingRequester.sol index 9b0ee37..85f3a5d 100644 --- a/contracts/vesting/SymmVestingRequester.sol +++ b/contracts/vesting/SymmVestingRequester.sol @@ -14,9 +14,9 @@ contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeab bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); - uint256 public totalTime = 180 days; + uint256 public totalDays = 180 days; uint256 public penaltyPerDay = 25e16; // 0.25e15 - uint256 public launchTime; + uint256 public launchDay; uint256 public totalRegisteredAmount = 0; uint256 public totalRegisteredUsers = 0; uint256 public totalVestedAmount = 0; @@ -24,7 +24,7 @@ contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeab address public symmAddress; address public symmVestingAddress; - function initializer(address admin, address _symmAddress, address _symmVestingAddress) public initializer{ + function initialize(address admin, address _symmAddress, address _symmVestingAddress) public initializer{ __AccessControlEnumerable_init(); __Pausable_init(); @@ -34,7 +34,7 @@ contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeab symmAddress = _symmAddress; symmVestingAddress = _symmVestingAddress; - launchTime = block.timestamp; + launchDay = (block.timestamp / 1 days) * 1 days; } function registerPlans(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { //TODO: whenNotPaused? @@ -45,7 +45,7 @@ contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeab totalRegisteredAmount -= registeredAmounts[users[i]] - amounts[i]; else totalRegisteredAmount += amounts[i] - registeredAmounts[users[i]]; - if(registeredAmounts[users[i]]!=0) totalRegisteredUsers += 1;//TODO: How to check it's not already added? + if(registeredAmounts[users[i]]!=0) totalRegisteredUsers += 1;//TODO: How to check it's not already added? add them to a mapping?! registeredAmounts[users[i]] = amounts[i]; } } @@ -76,8 +76,9 @@ contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeab } function _getEndTime() private view returns(uint256){ - uint256 timePassed = (block.timestamp / 1days - launchTime / 1days) * 1days; - if(timePassed > totalTime) timePassed = totalTime; - return block.timestamp + totalTime - timePassed + timePassed * penaltyPerDay / 1e18; + uint256 today = (block.timestamp / 1 days) * 1 days; + uint256 daysPassed = today - launchDay; + if(daysPassed > totalDays) daysPassed = totalDays; + return today + totalDays - daysPassed + daysPassed * penaltyPerDay / 1e18; } } From 3731009479d933f2b77dd3968e39512af0e5032f Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 14 Apr 2025 11:38:53 +0330 Subject: [PATCH 05/48] Change to non-upgradable --- contracts/vesting/SymmVestingRequester.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingRequester.sol index 85f3a5d..f2a7bc7 100644 --- a/contracts/vesting/SymmVestingRequester.sol +++ b/contracts/vesting/SymmVestingRequester.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.18; -import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; import {IVesting} from "./interfaces/IVesting.sol"; -contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable { +contract SymmVestingRequester is AccessControlEnumerable, Pausable{ error MismatchedArrays(); error ZeroAmount(); @@ -24,10 +24,7 @@ contract SymmVestingRequester is Initializable, AccessControlEnumerableUpgradeab address public symmAddress; address public symmVestingAddress; - function initialize(address admin, address _symmAddress, address _symmVestingAddress) public initializer{ - __AccessControlEnumerable_init(); - __Pausable_init(); - + constructor(address admin, address _symmAddress, address _symmVestingAddress){ _grantRole(SETTER_ROLE, admin); _grantRole(PAUSER_ROLE, admin); _grantRole(UNPAUSER_ROLE, admin); From 21830a87cfa3092076b784fa70a6abacb141f83f Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 14 Apr 2025 11:42:38 +0330 Subject: [PATCH 06/48] Change visiblity of getEndTime --- contracts/vesting/SymmVestingRequester.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingRequester.sol index f2a7bc7..1be9da9 100644 --- a/contracts/vesting/SymmVestingRequester.sol +++ b/contracts/vesting/SymmVestingRequester.sol @@ -56,7 +56,7 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ IVesting(symmVestingAddress).setupVestingPlans( symmAddress, block.timestamp, - _getEndTime(), + getEndTime(), users, amounts ); @@ -72,7 +72,7 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ _unpause(); } - function _getEndTime() private view returns(uint256){ + function getEndTime() public view returns(uint256){ uint256 today = (block.timestamp / 1 days) * 1 days; uint256 daysPassed = today - launchDay; if(daysPassed > totalDays) daysPassed = totalDays; From b268cc0a83235fb5abb5409995fd4d615bd76dce Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 14 Apr 2025 14:52:39 +0330 Subject: [PATCH 07/48] Change names --- contracts/vesting/SymmVestingRequester.sol | 51 ++++++++++++---------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingRequester.sol index 1be9da9..abf829a 100644 --- a/contracts/vesting/SymmVestingRequester.sol +++ b/contracts/vesting/SymmVestingRequester.sol @@ -14,17 +14,20 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); - uint256 public totalDays = 180 days; - uint256 public penaltyPerDay = 25e16; // 0.25e15 - uint256 public launchDay; - uint256 public totalRegisteredAmount = 0; - uint256 public totalRegisteredUsers = 0; - uint256 public totalVestedAmount = 0; - mapping(address=>uint256) public registeredAmounts; // user => amount - address public symmAddress; - address public symmVestingAddress; + uint256 public constant TOTAL_DAYS = 180 days; + uint256 public constant PENALTY_PER_DAY = 25e16; // 0.25e15 - constructor(address admin, address _symmAddress, address _symmVestingAddress){ + uint256 public immutable launchDay; + uint256 public immutable maxSymmAmount; + address public immutable symmAddress; + address public immutable symmVestingAddress; + + uint256 public initiatableAmountsSum = 0; + uint256 public usersInitiatedCount = 0; + uint256 public totalVestedAmount = 0; //TODO: totalInitiatedAmount? + mapping(address=>uint256) public initiatableAmount; // user => amount //TODO: Can be renamed to pendingVestingPlan + + constructor(address admin, address _symmAddress, address _symmVestingAddress, uint256 _totalInitiatableSYMM){ _grantRole(SETTER_ROLE, admin); _grantRole(PAUSER_ROLE, admin); _grantRole(UNPAUSER_ROLE, admin); @@ -32,27 +35,28 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ symmAddress = _symmAddress; symmVestingAddress = _symmVestingAddress; launchDay = (block.timestamp / 1 days) * 1 days; + maxSymmAmount = _totalInitiatableSYMM; } - function registerPlans(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { //TODO: whenNotPaused? + function setInitiatableVestingAmount(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { //TODO: whenNotPaused? if(users.length != amounts.length) revert MismatchedArrays(); for(uint32 i=0; i amounts[i]) - totalRegisteredAmount -= registeredAmounts[users[i]] - amounts[i]; + if(initiatableAmount[users[i]] > amounts[i]) + initiatableAmountsSum -= initiatableAmount[users[i]] - amounts[i]; else - totalRegisteredAmount += amounts[i] - registeredAmounts[users[i]]; - if(registeredAmounts[users[i]]!=0) totalRegisteredUsers += 1;//TODO: How to check it's not already added? add them to a mapping?! - registeredAmounts[users[i]] = amounts[i]; + initiatableAmountsSum += amounts[i] - initiatableAmount[users[i]]; + if(initiatableAmount[users[i]]!=0) usersInitiatedCount += 1;//TODO: How to check it's not already added? add them to a mapping?! + initiatableAmount[users[i]] = amounts[i]; } } - function requestVestingPlan() external whenNotPaused{ - if(registeredAmounts[msg.sender] == 0) revert ZeroAmount(); + function initiateVestingPlan() external whenNotPaused { + if(initiatableAmount[msg.sender] == 0) revert ZeroAmount(); address[] memory users; uint256[] memory amounts; users[0] = msg.sender; - amounts[0] = registeredAmounts[msg.sender]; + amounts[0] = initiatableAmount[msg.sender]; IVesting(symmVestingAddress).setupVestingPlans( symmAddress, block.timestamp, @@ -60,8 +64,9 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ users, amounts ); - totalVestedAmount += registeredAmounts[msg.sender]; - registeredAmounts[msg.sender] = 0; + totalVestedAmount += initiatableAmount[msg.sender]; + if(totalVestedAmount > maxSymmAmount) + initiatableAmount[msg.sender] = 0; } function pause() external onlyRole(PAUSER_ROLE) { @@ -75,7 +80,7 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ function getEndTime() public view returns(uint256){ uint256 today = (block.timestamp / 1 days) * 1 days; uint256 daysPassed = today - launchDay; - if(daysPassed > totalDays) daysPassed = totalDays; - return today + totalDays - daysPassed + daysPassed * penaltyPerDay / 1e18; + if(daysPassed > TOTAL_DAYS) daysPassed = TOTAL_DAYS; + return today + TOTAL_DAYS - daysPassed + daysPassed * PENALTY_PER_DAY / 1e18; } } From f4eb26909eb30690ed9ebfae4bcc3ab8891b8092 Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 14 Apr 2025 15:04:20 +0330 Subject: [PATCH 08/48] Add maxSymm and launchDay parameter --- contracts/vesting/SymmVestingRequester.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingRequester.sol index abf829a..55ddd0b 100644 --- a/contracts/vesting/SymmVestingRequester.sol +++ b/contracts/vesting/SymmVestingRequester.sol @@ -9,6 +9,7 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ error MismatchedArrays(); error ZeroAmount(); + error exceededMaxSymmAmount(uint256 exceededAmont, uint256 maxSymmAmount); bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); @@ -27,31 +28,36 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ uint256 public totalVestedAmount = 0; //TODO: totalInitiatedAmount? mapping(address=>uint256) public initiatableAmount; // user => amount //TODO: Can be renamed to pendingVestingPlan - constructor(address admin, address _symmAddress, address _symmVestingAddress, uint256 _totalInitiatableSYMM){ + constructor(address admin, address _symmAddress, address _symmVestingAddress, uint256 _totalInitiatableSYMM, uint256 launchTimestamp){ _grantRole(SETTER_ROLE, admin); _grantRole(PAUSER_ROLE, admin); _grantRole(UNPAUSER_ROLE, admin); symmAddress = _symmAddress; symmVestingAddress = _symmVestingAddress; - launchDay = (block.timestamp / 1 days) * 1 days; + launchDay = (launchTimestamp / 1 days) * 1 days; maxSymmAmount = _totalInitiatableSYMM; } function setInitiatableVestingAmount(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { //TODO: whenNotPaused? if(users.length != amounts.length) revert MismatchedArrays(); + for(uint32 i=0; i amounts[i]) initiatableAmountsSum -= initiatableAmount[users[i]] - amounts[i]; else initiatableAmountsSum += amounts[i] - initiatableAmount[users[i]]; + + if(initiatableAmountsSum > maxSymmAmount) revert exceededMaxSymmAmount(initiatableAmountsSum, maxSymmAmount); if(initiatableAmount[users[i]]!=0) usersInitiatedCount += 1;//TODO: How to check it's not already added? add them to a mapping?! + initiatableAmount[users[i]] = amounts[i]; } } function initiateVestingPlan() external whenNotPaused { + //TODO: custom error for checking whether launchDay is reached or not is not gas efficient due to underflow in getEndTime when launchTime is not reached if(initiatableAmount[msg.sender] == 0) revert ZeroAmount(); address[] memory users; uint256[] memory amounts; @@ -65,7 +71,7 @@ contract SymmVestingRequester is AccessControlEnumerable, Pausable{ amounts ); totalVestedAmount += initiatableAmount[msg.sender]; - if(totalVestedAmount > maxSymmAmount) + if(totalVestedAmount > maxSymmAmount) revert exceededMaxSymmAmount(totalVestedAmount, maxSymmAmount); initiatableAmount[msg.sender] = 0; } From 850720bb3a20050e4d9414ca54d3af1878d0c0b8 Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 14 Apr 2025 15:16:56 +0330 Subject: [PATCH 09/48] Rename the contract --- ...{SymmVestingRequester.sol => SymmVestingPlanInitializer.sol} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/vesting/{SymmVestingRequester.sol => SymmVestingPlanInitializer.sol} (98%) diff --git a/contracts/vesting/SymmVestingRequester.sol b/contracts/vesting/SymmVestingPlanInitializer.sol similarity index 98% rename from contracts/vesting/SymmVestingRequester.sol rename to contracts/vesting/SymmVestingPlanInitializer.sol index 55ddd0b..0261466 100644 --- a/contracts/vesting/SymmVestingRequester.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -5,7 +5,7 @@ import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import {IVesting} from "./interfaces/IVesting.sol"; -contract SymmVestingRequester is AccessControlEnumerable, Pausable{ +contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable{ error MismatchedArrays(); error ZeroAmount(); From 97cd2c1ce1ae1ececc5b0b73d4c5c1bdb2c77e51 Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 14 Apr 2025 16:53:57 +0330 Subject: [PATCH 10/48] Add userVestedAmount --- contracts/vesting/SymmVestingPlanInitializer.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol index 0261466..06046c2 100644 --- a/contracts/vesting/SymmVestingPlanInitializer.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -27,6 +27,7 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable{ uint256 public usersInitiatedCount = 0; uint256 public totalVestedAmount = 0; //TODO: totalInitiatedAmount? mapping(address=>uint256) public initiatableAmount; // user => amount //TODO: Can be renamed to pendingVestingPlan + mapping(address=>uint256) public userVestedAmount; // user => vested amount constructor(address admin, address _symmAddress, address _symmVestingAddress, uint256 _totalInitiatableSYMM, uint256 launchTimestamp){ _grantRole(SETTER_ROLE, admin); @@ -72,6 +73,7 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable{ ); totalVestedAmount += initiatableAmount[msg.sender]; if(totalVestedAmount > maxSymmAmount) revert exceededMaxSymmAmount(totalVestedAmount, maxSymmAmount); + userVestedAmount[msg.sender] += initiatableAmount[msg.sender]; initiatableAmount[msg.sender] = 0; } From d0ab8e4ba99cebd4625e5cbc25e3e727113fcc74 Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Wed, 16 Apr 2025 20:24:09 +0330 Subject: [PATCH 11/48] Rename files --- tasks/symmVestingPlanInitializer.ts | 21 ++++++ tests/symmVestingPlanInitializer.behavior.ts | 79 ++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tasks/symmVestingPlanInitializer.ts create mode 100644 tests/symmVestingPlanInitializer.behavior.ts diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts new file mode 100644 index 0000000..1ecae5f --- /dev/null +++ b/tasks/symmVestingPlanInitializer.ts @@ -0,0 +1,21 @@ +import {task} from "hardhat/config"; +import {HardhatRuntimeEnvironment} from "hardhat/types"; + +task("deploy:SymmVestingRequester", "Deploys the SymmVestingRequester contract") + .addParam("admin", "The admin of SymmVestingRequester") + .addParam("symm_token_address", "Address of the symm token") + .addParam("symm_vesting_address", "Address of the symmVestingContract") + .setAction(async ({admin, symmAddress, symmVestingAddress}, { ethers, upgrades }: HardhatRuntimeEnvironment) => { + console.log("deploy:SymmVestingRequester"); + + const SymmVestingRequester = await ethers.getContractFactory("SymmVestingRequester"); + const symmVestingRequester = await upgrades.deployProxy(SymmVestingRequester, [admin, symmAddress, symmVestingAddress], { + unsafeAllow: ["external-library-linking"], + initializer: "initialize", + }) + await symmVestingRequester.waitForDeployment(); + + console.log(`SymmVestingRequester Contract deployed at: ${await symmVestingRequester.getAddress()}`) + return symmVestingRequester + } +) \ No newline at end of file diff --git a/tests/symmVestingPlanInitializer.behavior.ts b/tests/symmVestingPlanInitializer.behavior.ts new file mode 100644 index 0000000..550f0d4 --- /dev/null +++ b/tests/symmVestingPlanInitializer.behavior.ts @@ -0,0 +1,79 @@ +import { expect } from "chai" +import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers" +import { SymmVestingRequester, Vesting } from "../typechain-types" +import { initializeFixture, RunContext } from "./Initialize.fixture" +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" +import { ethers } from "hardhat" + +export function shouldBehaveLikeSymmVestingRequester() { + let context: RunContext + let vestingRequester: SymmVestingRequester + let vesting: Vesting + let admin: SignerWithAddress + let setter: SignerWithAddress + let user1: SignerWithAddress + let user2: SignerWithAddress + + beforeEach(async () => { + context = await loadFixture(initializeFixture) + vestingRequester = context.symmVestingRequester + vesting = context.vesting + admin = context.signers.admin + setter = context.signers.setter + user1 = context.signers.user1 + user2 = context.signers.user2 + }) + + describe("registerPlans", () => { + it("should revert on mismatched array lengths", async () => { + await expect( + vestingRequester.connect(setter).registerPlans([user1.address], [100, 200]) + ).to.be.revertedWithCustomError(vestingRequester, "MismatchedArrays") + }) + + it("should register user allocations correctly", async () => { + await vestingRequester.connect(setter).registerPlans( + [user1.address, user2.address], + [1000, 2000] + ) + + expect(await vestingRequester.registeredAmounts(user1.address)).to.equal(1000) + expect(await vestingRequester.registeredAmounts(user2.address)).to.equal(2000) + }) + }) + + describe("requestVestingPlan", () => { + it("should revert if user not registered", async () => { + await expect( + vestingRequester.connect(user1).requestVestingPlan() + ).to.be.revertedWithCustomError(vestingRequester, "ZeroAmount") + }) + + it("should call setupVestingPlans and clear amount", async () => { + await vestingRequester.connect(setter).registerPlans([user1.address], [1000]) + + const tx = await vestingRequester.connect(user1).requestVestingPlan() + await tx.wait() + + expect(await vestingRequester.registeredAmounts(user1.address)).to.equal(0) + // Additional validation depends on mocking or reading from vesting contract if testable + }) + }) + + describe("pause/unpause", () => { + it("should allow pausing and prevent vesting requests while paused", async () => { + await vestingRequester.connect(setter).registerPlans([user1.address], [1000]) + await vestingRequester.connect(admin).pause() + + await expect( + vestingRequester.connect(user1).requestVestingPlan() + ).to.be.revertedWith("Pausable: paused") + + await vestingRequester.connect(admin).unpause() + + await expect( + vestingRequester.connect(user1).requestVestingPlan() + ).to.not.be.reverted + }) + }) +} From b8bfdc2067ce0703040200cdf755ba1c738609bc Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Thu, 17 Apr 2025 01:31:12 +0330 Subject: [PATCH 12/48] Initialize test and task for Initializer --- tasks/symmVestingPlanInitializer.ts | 23 ++++++++++---------- tests/Initialize.fixture.ts | 11 +++++++++- tests/symmVesting.behavior.ts | 2 +- tests/symmVestingPlanInitializer.behavior.ts | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts index 1ecae5f..96941a7 100644 --- a/tasks/symmVestingPlanInitializer.ts +++ b/tasks/symmVestingPlanInitializer.ts @@ -1,21 +1,22 @@ import {task} from "hardhat/config"; import {HardhatRuntimeEnvironment} from "hardhat/types"; -task("deploy:SymmVestingRequester", "Deploys the SymmVestingRequester contract") - .addParam("admin", "The admin of SymmVestingRequester") - .addParam("symm_token_address", "Address of the symm token") - .addParam("symm_vesting_address", "Address of the symmVestingContract") - .setAction(async ({admin, symmAddress, symmVestingAddress}, { ethers, upgrades }: HardhatRuntimeEnvironment) => { - console.log("deploy:SymmVestingRequester"); +task("deploy:SymmVestingPlanInitializer", "Deploys the SymmVestingPlanInitializer contract") + .addParam("admin", "The admin of SymmVestingPlanInitializer") + .addParam("symmTokenAddress", "Address of the symm token") + .addParam("symmVestingAddress", "Address of the symmVestingContract") + .addParam("launchTimeStamp", "The of the launch in seconds") + .setAction(async ({admin, symmAddress, symmVestingAddress, launchTimeStamp}, { ethers, upgrades }: HardhatRuntimeEnvironment) => { + console.log("deploy:SymmVestingPlanInitializer"); - const SymmVestingRequester = await ethers.getContractFactory("SymmVestingRequester"); - const symmVestingRequester = await upgrades.deployProxy(SymmVestingRequester, [admin, symmAddress, symmVestingAddress], { + const SymmVestingPlanInitializer = await ethers.getContractFactory("SymmVestingPlanInitializer"); + const symmVestingPlanInitializer = await upgrades.deployProxy(SymmVestingPlanInitializer, [admin, symmAddress, symmVestingAddress, launchTimeStamp], { unsafeAllow: ["external-library-linking"], initializer: "initialize", }) - await symmVestingRequester.waitForDeployment(); + await symmVestingPlanInitializer.waitForDeployment(); - console.log(`SymmVestingRequester Contract deployed at: ${await symmVestingRequester.getAddress()}`) - return symmVestingRequester + console.log(`symmVestingPlanInitializer Contract deployed at: ${await symmVestingPlanInitializer.getAddress()}`) + return symmVestingPlanInitializer } ) \ No newline at end of file diff --git a/tests/Initialize.fixture.ts b/tests/Initialize.fixture.ts index 3a472a5..0f4fc6a 100644 --- a/tests/Initialize.fixture.ts +++ b/tests/Initialize.fixture.ts @@ -1,8 +1,9 @@ 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 { SymmAllocationClaimer, Symmio, Vesting, SymmStaking, SymmVestingRequester } from "../typechain-types"; import * as Process from "process"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; export class RunContext { signers!: { @@ -18,6 +19,7 @@ export class RunContext { claimSymm!: SymmAllocationClaimer vesting!: Vesting symmStaking!: SymmStaking + symmVestingVlanInitializer!: SymmVestingRequester } export async function initializeFixture(): Promise { @@ -64,6 +66,13 @@ export async function initializeFixture(): Promise { stakingToken: await context.symmioToken.getAddress(), }) + // context.symmVestingVlanInitializer = await run("deploy:SymmVestingPlanInitializer", { + // admin: await context.signers.admin.getAddress(), + // symmTokenAddress: await context.symmioToken.getAddress(), + // symmVestingAddress: await context.vesting.getAddress(), + // launchTimeStamp: (new Date().getTime() + 7 * 24 * 60 * 60).toString() + // }) + await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), context.signers.admin) const roles = [ diff --git a/tests/symmVesting.behavior.ts b/tests/symmVesting.behavior.ts index fe92387..1561e81 100644 --- a/tests/symmVesting.behavior.ts +++ b/tests/symmVesting.behavior.ts @@ -127,7 +127,7 @@ export function shouldBehaveLikeSymmVesting() { }) it("should revert if user does not have enough locked SYMM", async () => { - const symmAmount = String(800e18) + const symmAmount = String(999e18) const minLpAmount = 0 await expect(symmVesting.connect(user1).addLiquidity(symmAmount, minLpAmount, user1UsdcAmount)).to.be.revertedWithCustomError( symmVesting, diff --git a/tests/symmVestingPlanInitializer.behavior.ts b/tests/symmVestingPlanInitializer.behavior.ts index 550f0d4..711be59 100644 --- a/tests/symmVestingPlanInitializer.behavior.ts +++ b/tests/symmVestingPlanInitializer.behavior.ts @@ -16,7 +16,7 @@ export function shouldBehaveLikeSymmVestingRequester() { beforeEach(async () => { context = await loadFixture(initializeFixture) - vestingRequester = context.symmVestingRequester + vestingRequester = context.symmVestingVlanInitializer vesting = context.vesting admin = context.signers.admin setter = context.signers.setter From 32a288109eae384927758930c09b5eea9ff47b2a Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Sun, 20 Apr 2025 16:13:05 +0330 Subject: [PATCH 13/48] Add tests --- .../vesting/SymmVestingPlanInitializer.sol | 47 ++- tasks/symmVestingPlanInitializer.ts | 9 +- tests/Initialize.fixture.ts | 17 +- tests/main.ts | 5 + tests/symmVestingPlanInitializer.behavior.ts | 295 ++++++++++++++---- 5 files changed, 276 insertions(+), 97 deletions(-) diff --git a/contracts/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol index 06046c2..a506a6a 100644 --- a/contracts/vesting/SymmVestingPlanInitializer.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -9,50 +9,43 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable{ error MismatchedArrays(); error ZeroAmount(); - error exceededMaxSymmAmount(uint256 exceededAmont, uint256 maxSymmAmount); + error exceededMaxSymmAmount(uint256 exceededAmont, uint256 MaxVestedSymm); bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); uint256 public constant TOTAL_DAYS = 180 days; - uint256 public constant PENALTY_PER_DAY = 25e16; // 0.25e15 + uint256 public constant PENALTY_PER_DAY = 25e16; // 0.25e18 - uint256 public immutable launchDay; - uint256 public immutable maxSymmAmount; - address public immutable symmAddress; - address public immutable symmVestingAddress; + uint256 public immutable LAUNCH_DAY; + uint256 public immutable MAX_VESTED_SYMM; + address public immutable SYMM_ADDRESS; + address public immutable SYMM_VESTING_ADDRESS; uint256 public initiatableAmountsSum = 0; - uint256 public usersInitiatedCount = 0; - uint256 public totalVestedAmount = 0; //TODO: totalInitiatedAmount? mapping(address=>uint256) public initiatableAmount; // user => amount //TODO: Can be renamed to pendingVestingPlan mapping(address=>uint256) public userVestedAmount; // user => vested amount constructor(address admin, address _symmAddress, address _symmVestingAddress, uint256 _totalInitiatableSYMM, uint256 launchTimestamp){ + _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(SETTER_ROLE, admin); _grantRole(PAUSER_ROLE, admin); _grantRole(UNPAUSER_ROLE, admin); - symmAddress = _symmAddress; - symmVestingAddress = _symmVestingAddress; - launchDay = (launchTimestamp / 1 days) * 1 days; - maxSymmAmount = _totalInitiatableSYMM; + SYMM_ADDRESS = _symmAddress; + SYMM_VESTING_ADDRESS = _symmVestingAddress; + LAUNCH_DAY = (launchTimestamp / 1 days) * 1 days; + MAX_VESTED_SYMM = _totalInitiatableSYMM; } - function setInitiatableVestingAmount(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { //TODO: whenNotPaused? + function setInitiatableVestingAmount(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { if(users.length != amounts.length) revert MismatchedArrays(); for(uint32 i=0; i amounts[i]) - initiatableAmountsSum -= initiatableAmount[users[i]] - amounts[i]; - else - initiatableAmountsSum += amounts[i] - initiatableAmount[users[i]]; - - if(initiatableAmountsSum > maxSymmAmount) revert exceededMaxSymmAmount(initiatableAmountsSum, maxSymmAmount); - if(initiatableAmount[users[i]]!=0) usersInitiatedCount += 1;//TODO: How to check it's not already added? add them to a mapping?! - + initiatableAmountsSum = initiatableAmountsSum + amounts[i] - initiatableAmount[users[i]]; + if(initiatableAmountsSum > MAX_VESTED_SYMM) revert exceededMaxSymmAmount(initiatableAmountsSum, MAX_VESTED_SYMM); initiatableAmount[users[i]] = amounts[i]; } } @@ -60,19 +53,17 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable{ function initiateVestingPlan() external whenNotPaused { //TODO: custom error for checking whether launchDay is reached or not is not gas efficient due to underflow in getEndTime when launchTime is not reached if(initiatableAmount[msg.sender] == 0) revert ZeroAmount(); - address[] memory users; - uint256[] memory amounts; + address[] memory users = new address[](1); + uint256[] memory amounts = new uint256[](1); users[0] = msg.sender; amounts[0] = initiatableAmount[msg.sender]; - IVesting(symmVestingAddress).setupVestingPlans( - symmAddress, + IVesting(SYMM_VESTING_ADDRESS).setupVestingPlans( + SYMM_ADDRESS, block.timestamp, getEndTime(), users, amounts ); - totalVestedAmount += initiatableAmount[msg.sender]; - if(totalVestedAmount > maxSymmAmount) revert exceededMaxSymmAmount(totalVestedAmount, maxSymmAmount); userVestedAmount[msg.sender] += initiatableAmount[msg.sender]; initiatableAmount[msg.sender] = 0; } @@ -87,7 +78,7 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable{ function getEndTime() public view returns(uint256){ uint256 today = (block.timestamp / 1 days) * 1 days; - uint256 daysPassed = today - launchDay; + uint256 daysPassed = today - LAUNCH_DAY; if(daysPassed > TOTAL_DAYS) daysPassed = TOTAL_DAYS; return today + TOTAL_DAYS - daysPassed + daysPassed * PENALTY_PER_DAY / 1e18; } diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts index 96941a7..a48f9c5 100644 --- a/tasks/symmVestingPlanInitializer.ts +++ b/tasks/symmVestingPlanInitializer.ts @@ -5,15 +5,14 @@ task("deploy:SymmVestingPlanInitializer", "Deploys the SymmVestingPlanInitialize .addParam("admin", "The admin of SymmVestingPlanInitializer") .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 ({admin, symmAddress, symmVestingAddress, launchTimeStamp}, { ethers, upgrades }: HardhatRuntimeEnvironment) => { + .setAction(async ({admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp}, { ethers, upgrades }: HardhatRuntimeEnvironment) => { console.log("deploy:SymmVestingPlanInitializer"); const SymmVestingPlanInitializer = await ethers.getContractFactory("SymmVestingPlanInitializer"); - const symmVestingPlanInitializer = await upgrades.deployProxy(SymmVestingPlanInitializer, [admin, symmAddress, symmVestingAddress, launchTimeStamp], { - unsafeAllow: ["external-library-linking"], - initializer: "initialize", - }) + console.log(admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp) + const symmVestingPlanInitializer = await SymmVestingPlanInitializer.deploy(admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp) await symmVestingPlanInitializer.waitForDeployment(); console.log(`symmVestingPlanInitializer Contract deployed at: ${await symmVestingPlanInitializer.getAddress()}`) diff --git a/tests/Initialize.fixture.ts b/tests/Initialize.fixture.ts index 0f4fc6a..67cc670 100644 --- a/tests/Initialize.fixture.ts +++ b/tests/Initialize.fixture.ts @@ -4,6 +4,7 @@ import { e } from "../utils" import { SymmAllocationClaimer, Symmio, Vesting, SymmStaking, SymmVestingRequester } from "../typechain-types"; import * as Process from "process"; import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { floor } from "lodash"; export class RunContext { signers!: { @@ -65,13 +66,14 @@ export async function initializeFixture(): Promise { admin: await context.signers.admin.getAddress(), stakingToken: await context.symmioToken.getAddress(), }) - - // context.symmVestingVlanInitializer = await run("deploy:SymmVestingPlanInitializer", { - // admin: await context.signers.admin.getAddress(), - // symmTokenAddress: await context.symmioToken.getAddress(), - // symmVestingAddress: await context.vesting.getAddress(), - // launchTimeStamp: (new Date().getTime() + 7 * 24 * 60 * 60).toString() - // }) +console.log("in 7 days: " + String(floor(Date.now()/1000) + 7 * 24 * 60 * 60) +" - "+ (Date.now()/1000 + 7 * 24 * 60 * 60 ).toString()) + context.symmVestingVlanInitializer = await run("deploy:SymmVestingPlanInitializer", { + admin: await context.signers.admin.getAddress(), + symmTokenAddress: await context.symmioToken.getAddress(), + symmVestingAddress: await context.vesting.getAddress(), + totalInitiatableSYMM: "10000000000000000000000000", //10Me18 + launchTimeStamp: String(floor(Date.now()/1000) + 7 * 24 * 60 * 60) + }) await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), context.signers.admin) @@ -86,6 +88,7 @@ export async function initializeFixture(): Promise { 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 } diff --git a/tests/main.ts b/tests/main.ts index a1bad9d..26c7836 100644 --- a/tests/main.ts +++ b/tests/main.ts @@ -3,6 +3,7 @@ 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" describe("Symmio Token", () => { // if (process.env.TEST_MODE === "static") { @@ -26,6 +27,10 @@ describe("Symmio Token", () => { describe("Vesting", async function () { ShouldBehaveLikeVesting() }) + + describe.only("Symm Vesting Plan Initializer", async function () { + shouldBehaveLikeSymmVestingPlanInitializer() + }) }) // } else if (process.env.TEST_MODE === "dynamic") { // Dynamic tests diff --git a/tests/symmVestingPlanInitializer.behavior.ts b/tests/symmVestingPlanInitializer.behavior.ts index 711be59..3a1b1e3 100644 --- a/tests/symmVestingPlanInitializer.behavior.ts +++ b/tests/symmVestingPlanInitializer.behavior.ts @@ -1,79 +1,260 @@ -import { expect } from "chai" -import { loadFixture, time } from "@nomicfoundation/hardhat-network-helpers" -import { SymmVestingRequester, Vesting } from "../typechain-types" -import { initializeFixture, RunContext } from "./Initialize.fixture" -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" -import { ethers } from "hardhat" - -export function shouldBehaveLikeSymmVestingRequester() { - let context: RunContext - let vestingRequester: SymmVestingRequester - let vesting: Vesting - let admin: SignerWithAddress - let setter: SignerWithAddress - let user1: SignerWithAddress - let user2: SignerWithAddress +/* 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) - vestingRequester = context.symmVestingVlanInitializer - vesting = context.vesting - admin = context.signers.admin - setter = context.signers.setter - user1 = context.signers.user1 - user2 = context.signers.user2 - }) - - describe("registerPlans", () => { + context = await loadFixture(initializeFixture); + vestingPlanInitializer = context.symmVestingVlanInitializer; + ({ admin, user1, user2 } = context.signers); + launchTime = await vestingPlanInitializer.LAUNCH_DAY(); + }); + + /* ---------------------------------------------------------------------- */ + /* setInitiatableVestingAmount() tests */ + /* ---------------------------------------------------------------------- */ + describe("setInitiatableVestingAmount", () => { it("should revert on mismatched array lengths", async () => { await expect( - vestingRequester.connect(setter).registerPlans([user1.address], [100, 200]) - ).to.be.revertedWithCustomError(vestingRequester, "MismatchedArrays") - }) + vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1.address], [100, 200]) + ).to.be.revertedWithCustomError(vestingPlanInitializer, "MismatchedArrays"); + }); + + it("should reject callers without SETTER_ROLE", async () => { + await expect( + vestingPlanInitializer + .connect(user1) + .setInitiatableVestingAmount([user1.address], [1000]) + ).to.be.reverted; + }); it("should register user allocations correctly", async () => { - await vestingRequester.connect(setter).registerPlans( - [user1.address, user2.address], - [1000, 2000] - ) + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([ + user1.address, + user2.address, + ], [ + 1000, + 2000, + ]); - expect(await vestingRequester.registeredAmounts(user1.address)).to.equal(1000) - expect(await vestingRequester.registeredAmounts(user2.address)).to.equal(2000) - }) - }) + expect(await vestingPlanInitializer.initiatableAmount(user1.address)).to.equal(1000); + expect(await vestingPlanInitializer.initiatableAmount(user2.address)).to.equal(2000); + expect(await vestingPlanInitializer.initiatableAmountsSum()).to.equal(3000); - describe("requestVestingPlan", () => { - it("should revert if user not registered", async () => { + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([ + user1.address, + user2.address, + ], [ + 150, + 430, + ]); + + + expect(await vestingPlanInitializer.initiatableAmount(user1.address)).to.equal(150); + expect(await vestingPlanInitializer.initiatableAmount(user2.address)).to.equal(430); + expect(await vestingPlanInitializer.initiatableAmountsSum()).to.equal(430+150); + + }); + + it("should enforce the global SYMM cap", async () => { + const overCap = ethers.parseEther("10000001"); await expect( - vestingRequester.connect(user1).requestVestingPlan() - ).to.be.revertedWithCustomError(vestingRequester, "ZeroAmount") - }) + vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1.address], [overCap]) + ).to.be.revertedWithCustomError(vestingPlanInitializer, "exceededMaxSymmAmount"); + }); + }); + + /* ---------------------------------------------------------------------- */ + /* initiateVestingPlan() tests */ + /* ---------------------------------------------------------------------- */ + describe("initiateVestingPlan", () => { + beforeEach(async () => { + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1], [1000]); + }); - it("should call setupVestingPlans and clear amount", async () => { - await vestingRequester.connect(setter).registerPlans([user1.address], [1000]) + it("should revert if the caller has no initiatable amount", async () => { + await expect( + vestingPlanInitializer.connect(user2).initiateVestingPlan() + ).to.be.revertedWithCustomError(vestingPlanInitializer, "ZeroAmount"); + }); - const tx = await vestingRequester.connect(user1).requestVestingPlan() - await tx.wait() + it("should revert while the contract is paused", async () => { + await vestingPlanInitializer.connect(admin).pause(); + await expect( + vestingPlanInitializer.connect(user1).initiateVestingPlan() + ).to.be.revertedWithCustomError(vestingPlanInitializer, 'EnforcedPause'); + }); - expect(await vestingRequester.registeredAmounts(user1.address)).to.equal(0) - // Additional validation depends on mocking or reading from vesting contract if testable - }) - }) + it("should revert if launch time is not reached", async () => { + await expect( + vestingPlanInitializer.connect(user1).initiateVestingPlan() + ).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).initiateVestingPlan() + ).to.not.be.reverted; + expect(await vestingPlanInitializer.initiatableAmount(user1.address)).to.equal(0); + expect(await vestingPlanInitializer.userVestedAmount(user1.address)).to.equal(1000); + }); + }); + + /* ---------------------------------------------------------------------- */ + /* Pausing */ + /* ---------------------------------------------------------------------- */ describe("pause/unpause", () => { it("should allow pausing and prevent vesting requests while paused", async () => { - await vestingRequester.connect(setter).registerPlans([user1.address], [1000]) - await vestingRequester.connect(admin).pause() + await time.increaseTo(launchTime); + + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1], [1000]); + await vestingPlanInitializer.connect(admin).pause(); await expect( - vestingRequester.connect(user1).requestVestingPlan() - ).to.be.revertedWith("Pausable: paused") + vestingPlanInitializer.connect(user1).initiateVestingPlan() + ).to.be.revertedWithCustomError(vestingPlanInitializer, 'EnforcedPause'); - await vestingRequester.connect(admin).unpause() + await vestingPlanInitializer.connect(admin).unpause(); await expect( - vestingRequester.connect(user1).requestVestingPlan() - ).to.not.be.reverted + vestingPlanInitializer.connect(user1).initiateVestingPlan() + ).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") }) - }) + }); + + /* ---------------------------------------------------------------------- */ + /* getEndTime() */ + /* ---------------------------------------------------------------------- */ + describe("getEndTime()", () => { + it("should extend linearly with penalty as days pass", async () => { + await time.increaseTo(launchTime); + const before = await vestingPlanInitializer.getEndTime(); + 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.getEndTime(); + 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.getEndTime() + expect(endTime).to.equal(Number(launchTime) + oneHundredEightyDays + tenDays + 45 * oneDay) + }); + }); + + + /* ---------------------------------------------------------------------- */ + /* View variables */ + /* ---------------------------------------------------------------------- */ + describe("viewVariables", () => { + beforeEach(async() => { + await time.increaseTo(launchTime); + }) + + it("should calculate initiatableAmountsSum and userVestedAmount correctly while admin decreases initiatable amount", async () => { + const beforeInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + expect(beforeInitiatableAmountSum).to.equal(0) + + const userVestedAmount = await vestingPlanInitializer.userVestedAmount(user1) + expect(userVestedAmount).to.equal(0) + + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1], [1000]); + + const firstInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + expect(firstInitiatableAmountSum).to.equal(1000) + + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1], [250]); + + const secondInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + expect(secondInitiatableAmountSum).to.equal(250) + + await vestingPlanInitializer.connect(user1).initiateVestingPlan(); + + expect(await vestingPlanInitializer.initiatableAmount(user1)).to.equal(0) + expect(await vestingPlanInitializer.userVestedAmount(user1)).to.equal(250) + + }); + + it("should calculate initiatableAmountsSum and userVestedAmount correctly while admin increases initiatable amount", async () => { + const beforeInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + expect(beforeInitiatableAmountSum).to.equal(0) + + const userVestedAmount = await vestingPlanInitializer.userVestedAmount(user1) + expect(userVestedAmount).to.equal(0) + + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1], [1000]); + + const firstInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + expect(firstInitiatableAmountSum).to.equal(1000) + + + await vestingPlanInitializer + .connect(admin) + .setInitiatableVestingAmount([user1], [2500]); + + const secondInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + expect(secondInitiatableAmountSum).to.equal(2500) + + await vestingPlanInitializer.connect(user1).initiateVestingPlan(); + + expect(await vestingPlanInitializer.initiatableAmount(user1)).to.equal(0) + expect(await vestingPlanInitializer.userVestedAmount(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(); + expect(penaltyPerDay).to.equal(e("0.25")) + + const totalDays = await vestingPlanInitializer.TOTAL_DAYS(); + expect(totalDays).to.equal(180*24*60*60) + }) + + }); } From d90dcbe719acadf77d8bb6cdbce799d0ae4afb26 Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Mon, 21 Apr 2025 10:56:02 +0330 Subject: [PATCH 14/48] clean codes --- tasks/symmVestingPlanInitializer.ts | 1 - tests/Initialize.fixture.ts | 2 +- tests/main.ts | 2 +- tests/symmVesting.behavior.ts | 2 +- tests/symmVestingPlanInitializer.behavior.ts | 2 +- tests/symmVestingRequester.behavior.ts | 0 6 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 tests/symmVestingRequester.behavior.ts diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts index a48f9c5..6970c91 100644 --- a/tasks/symmVestingPlanInitializer.ts +++ b/tasks/symmVestingPlanInitializer.ts @@ -11,7 +11,6 @@ task("deploy:SymmVestingPlanInitializer", "Deploys the SymmVestingPlanInitialize console.log("deploy:SymmVestingPlanInitializer"); const SymmVestingPlanInitializer = await ethers.getContractFactory("SymmVestingPlanInitializer"); - console.log(admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp) const symmVestingPlanInitializer = await SymmVestingPlanInitializer.deploy(admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp) await symmVestingPlanInitializer.waitForDeployment(); diff --git a/tests/Initialize.fixture.ts b/tests/Initialize.fixture.ts index 67cc670..6ff1a33 100644 --- a/tests/Initialize.fixture.ts +++ b/tests/Initialize.fixture.ts @@ -66,7 +66,7 @@ export async function initializeFixture(): Promise { admin: await context.signers.admin.getAddress(), stakingToken: await context.symmioToken.getAddress(), }) -console.log("in 7 days: " + String(floor(Date.now()/1000) + 7 * 24 * 60 * 60) +" - "+ (Date.now()/1000 + 7 * 24 * 60 * 60 ).toString()) + context.symmVestingVlanInitializer = await run("deploy:SymmVestingPlanInitializer", { admin: await context.signers.admin.getAddress(), symmTokenAddress: await context.symmioToken.getAddress(), diff --git a/tests/main.ts b/tests/main.ts index 26c7836..4373a36 100644 --- a/tests/main.ts +++ b/tests/main.ts @@ -28,7 +28,7 @@ describe("Symmio Token", () => { ShouldBehaveLikeVesting() }) - describe.only("Symm Vesting Plan Initializer", async function () { + describe("Symm Vesting Plan Initializer", async function () { shouldBehaveLikeSymmVestingPlanInitializer() }) }) diff --git a/tests/symmVesting.behavior.ts b/tests/symmVesting.behavior.ts index 1561e81..fe92387 100644 --- a/tests/symmVesting.behavior.ts +++ b/tests/symmVesting.behavior.ts @@ -127,7 +127,7 @@ export function shouldBehaveLikeSymmVesting() { }) it("should revert if user does not have enough locked SYMM", async () => { - const symmAmount = String(999e18) + const symmAmount = String(800e18) const minLpAmount = 0 await expect(symmVesting.connect(user1).addLiquidity(symmAmount, minLpAmount, user1UsdcAmount)).to.be.revertedWithCustomError( symmVesting, diff --git a/tests/symmVestingPlanInitializer.behavior.ts b/tests/symmVestingPlanInitializer.behavior.ts index 3a1b1e3..0a65f2d 100644 --- a/tests/symmVestingPlanInitializer.behavior.ts +++ b/tests/symmVestingPlanInitializer.behavior.ts @@ -10,7 +10,7 @@ import { BigNumberish } from "ethers"; import { e } from "../utils"; -export function shouldBehaveLikeSymmVestingPlanInitializer() { +export function shouldBehaveLikeSymmVestingPlanInitializer() { let context: RunContext; let vestingPlanInitializer: SymmVestingPlanInitializer; // keep the same naming pattern the user showed let admin: SignerWithAddress; diff --git a/tests/symmVestingRequester.behavior.ts b/tests/symmVestingRequester.behavior.ts deleted file mode 100644 index e69de29..0000000 From de9d5ee56863ec2a78a37c9641af6888b994d30d Mon Sep 17 00:00:00 2001 From: timaster Date: Tue, 22 Apr 2025 11:42:43 +0330 Subject: [PATCH 15/48] Write deploy script for symmVestingPlanInitializer (#SYM-377) --- .gitignore | 5 +- hardhat.config.ts | 6 +- package-lock.json | 3 - scripts/user_allocation.py | 60 + tasks/index.ts | 6 + tasks/symmVestingPlanInitializer.ts | 56 +- user_available_symm.json | 3200 +++++++++++++++++++++++++++ 7 files changed, 3315 insertions(+), 21 deletions(-) create mode 100644 scripts/user_allocation.py create mode 100644 tasks/index.ts create mode 100644 user_available_symm.json diff --git a/.gitignore b/.gitignore index 3673f3d..40a9252 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,7 @@ ignition/deployments/chain-31337 .idea *.csv package-lock.json -rewards \ No newline at end of file +rewards + +abis +error.log \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 833fada..104e1fa 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -7,11 +7,7 @@ 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] diff --git a/package-lock.json b/package-lock.json index d909bc6..3d62580 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,6 @@ "dependencies": { "csv-parse": "^5.6.0" }, - "dependencies": { - "csv-parse": "^5.6.0" - }, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^2.0.7", "@nomicfoundation/hardhat-ethers": "^3.0.8", diff --git a/scripts/user_allocation.py b/scripts/user_allocation.py new file mode 100644 index 0000000..fb311b1 --- /dev/null +++ b/scripts/user_allocation.py @@ -0,0 +1,60 @@ +import json +import os + +import requests +from multicallable import Multicallable +from web3 import Web3 + +SUBGRAPH_URL = "https://api.studio.thegraph.com/query/85206/allocated/version/latest" + +rpc = "https://base-rpc.publicnode.com" +rpc = "https://base.llamarpc.com" +rpc = "https://base.drpc.org" + +contract_address = "0x232b72527e3692E78d7f6D73634fc4E100E31f80" + +w3 = Web3(Web3.HTTPProvider(rpc)) + + +def fetch_all_users(): + all_users = set() + last_id = "" + while True: + query = f""" + {{ + batchAllocationsSets(orderBy: id, orderDirection: asc, first: 1000, where: {{id_gt: "{last_id}"}}) {{ + id + users + }} + }} + """ + response = requests.post(SUBGRAPH_URL, json={'query': query}) + data = response.json() + sets = data["data"]["batchAllocationsSets"] + + if not sets: + break + + for batch in sets: + for user in batch["users"]: + all_users.add(w3.to_checksum_address(user)) + + last_id = sets[-1]["id"] + + return list(all_users) + + +users = fetch_all_users() +print(f"Fetched {len(users)} unique users.") + +with open(f"{os.getcwd()}/abis/SymmAllocationClaimer.json", "r") as f: + abi = json.load(f) + +contract = Multicallable(w3.to_checksum_address(contract_address), abi, w3) +available = contract.userAllocations(users).call(n=len(users) // 200 + 1, progress_bar=True) +rows = {"Users": [], "Available": []} +for user, amount in zip(users, available): + rows["Users"].append(user) + rows["Available"].append(str(amount)) +with open(f"{os.getcwd()}/user_available_symm.json", "w") as f: + f.write(json.dumps(rows, indent=2)) diff --git a/tasks/index.ts b/tasks/index.ts new file mode 100644 index 0000000..df2c488 --- /dev/null +++ b/tasks/index.ts @@ -0,0 +1,6 @@ +import "./symmVestingPlanInitializer" +import "./symmioToken" +import "./symmStaking" +import "./symmAllocationClaimer" +import "./symmVestingRequester" +import "./symmVesting" \ No newline at end of file diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts index 6970c91..9ba6934 100644 --- a/tasks/symmVestingPlanInitializer.ts +++ b/tasks/symmVestingPlanInitializer.ts @@ -1,20 +1,52 @@ -import {task} from "hardhat/config"; -import {HardhatRuntimeEnvironment} from "hardhat/types"; +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("admin", "The admin of SymmVestingPlanInitializer") .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 ({admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp}, { ethers, upgrades }: HardhatRuntimeEnvironment) => { - console.log("deploy:SymmVestingPlanInitializer"); + .setAction(async ({ symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp }, { + ethers, + upgrades, + }: HardhatRuntimeEnvironment) => { + console.log("deploy:SymmVestingPlanInitializer"); - const SymmVestingPlanInitializer = await ethers.getContractFactory("SymmVestingPlanInitializer"); - const symmVestingPlanInitializer = await SymmVestingPlanInitializer.deploy(admin, symmTokenAddress, symmVestingAddress, totalInitiatableSYMM, launchTimeStamp) - await symmVestingPlanInitializer.waitForDeployment(); + const signers = await ethers.getSigners(); + const admin = signers[0]; - console.log(`symmVestingPlanInitializer Contract deployed at: ${await symmVestingPlanInitializer.getAddress()}`) - return symmVestingPlanInitializer - } -) \ No newline at end of file + 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 = 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(`symmVestingPlanInitializer Contract deployed at: ${await symmVestingPlanInitializer.getAddress()}`); + return symmVestingPlanInitializer; + }, + ); diff --git a/user_available_symm.json b/user_available_symm.json new file mode 100644 index 0000000..8e1b714 --- /dev/null +++ b/user_available_symm.json @@ -0,0 +1,3200 @@ +{ + "Users": [ + "0xAf372B93a02C54E231A9147869EF11F21E0FE82A", + "0x37633c9F778c44e852914c7056cBBBF75323Dfd6", + "0x24438f748083CB8a5f967d6B726C9aEeDF78ABa3", + "0x408A64365229C6097DAB7f23C8e2bBC906951567", + "0x4f8Efb4CC83a83e908029dB0a4c782B651a3BB83", + "0x69c509e9765c49CC6B9B4568b90Aa47B89F4992f", + "0x51f059C07E03d330B007B48630a857E582BE8236", + "0xBcbfD32b980E95d765C49728960DA9f4504947d7", + "0x7A7528C26CaDf1c4Cb3014f1A99f5F3204A77333", + "0x9633BAc0ae8d720E2016D8268a9030A6aCadD784", + "0x5942A257Bd448670Ae4353b06bAcDAbAb5FBF0c8", + "0x534b86280545b270b0B0fC2A57096C42290b801F", + "0x60A2E0ddAC932aC61E38B9cA80aa75926966e01D", + "0x941689a3677aE5D7547aA6b298773db1a50E2Ebc", + "0xB5f69d0cBD702D713767C5442122c156bD6C971c", + "0xB5e0C5A26Ed9cD9c1684Fbb1138D5952C53349F8", + "0x6573948AEf66B3EB45f35AB5C7516678F86d7CC2", + "0xA0a3eae0F173567B9ea2abD8b53099AE09d71114", + "0x34457cA3A154415eE1847d311810B33815Dc7497", + "0xCaF76ca5cF79510F7928EEE79EfEB92372957c10", + "0x6ffbb6C376d0cf590289671D652Ae92213614828", + "0xe0ad5F6422687bf667D09590383FdC3C0f8aF392", + "0xECCb9B9C6fb7590a4d0588953B3170A1a84E3341", + "0x8a6a3Fe565451f72876d88Ba55C6152bBb0c1836", + "0x062B0613A91603975B330C4Fa4bbd17feeEfD130", + "0x2d13CD9A79e68974993D19451fe615925557FE28", + "0x7A505dF95aAc78f142CA98CeCd771d3c398b4b71", + "0xa973d0968d6BbE4859baD50379d87c91Be615B9C", + "0xaF1ecb33e5B49F74c020C0E5285d73504B01E051", + "0xAd52E66a1ad6Efe114Cb9278dbF9338e37A517e5", + "0x4f4b9492392b88a1D21611575B732428a1BeDEA6", + "0x225948313D4CC7B5953Ad698740eC333Df52a8f4", + "0x1255E17ffF07322231A4124Bb455C85d20d0a2c4", + "0xe175c44341736349fcB4c1498eB27d77b96d7Bb3", + "0x98ed1eAE1836Fa93A20FA389Fc386bA75bd07D5B", + "0xA8c24C16ad11D008eba1695D37417b47aB461a98", + "0x69f8D754C5f4F73aad00f3C22EaFB77Aa57Ff1BC", + "0x6187053cb6Ee01e5d834A0A039dA91060Cac1Cb0", + "0xf43257DF6B2066C866ceee61218ee1Ee50D3be6f", + "0xDAADaf4F2D94cdd4274eecF794C3d5c27F647476", + "0x44f4DA18D1e9609E13B3d10cD091e3836C69Bff2", + "0xDee7e15ff122C733c1f5E8800187815391c8bBc7", + "0x848dabf4F7097617AA482110615E5E84e4D5DCDD", + "0xACF902451683Bbc6f1bd5727a8e7E6B03e5D08e9", + "0xA02D5126317c5cE85ACd8B0ad1d35f944Beb441a", + "0xE72C1ADaBEbA9F743Af60D4dEb45bcFa090a5320", + "0x679bCDAe56EDf5c847e769Ca9eB82Ab58C93351a", + "0x65B064726FFfff3b3667718d2264E84367Be5141", + "0xa219712cC2Aaa5AA98CcF2A7ba055231F1752323", + "0xa25e3A5F8268538c3726CcEc9624056973b71d2F", + "0x818EAC5be158d2629702051E4B8c24Cb27829AB4", + "0xE9E2C06423d41e91e63DE4Ab4A68b3B4db0e681a", + "0x7ADFCCFd3691A99530F632EEc3dC480419ADdd7d", + "0xA9433Fe051aeE57B1DE0283504bE74B1D6cD39ab", + "0xd8fA3942e3CE77B6f98Fb17B2564d273C0b67d1b", + "0x8922923059388a4488Fa3b7C418BD059723bD37F", + "0x021F1c3677c19B13093eB39C445A4847500260E7", + "0x29F82d09C2AfD12f3c10ee49CD713331f4A7228E", + "0x2CD3a242D493Bb8aE3369d3EE2b83B5bf4fe82D7", + "0x4acCC4f0781594dEB1C382EA8a04B566927CfbED", + "0x7B3fC9597f146F0A80FC26dB0DdF62C04ea89740", + "0x79BE4bAe05714B8F50C840A2B4bd4D8B52f4233F", + "0x003cdd05da057a884803D1ba3f5De8aFdAcf73Ea", + "0xB444776908Ff797E69D516caa5CC22856781B0A9", + "0x2B2843A5CBCa8ece395665F3c545707Ad4b3d155", + "0xd2cbB2EfC68c8F7819807EAe033CD5c84Af8e235", + "0x06ac42915daFd5166505377713b849Ad85b56a87", + "0x95DB2fEf9F52Ba3F8CF4548374b9135dEE83E8c5", + "0x629f6a7335531791eBeD6E7651634285c3280334", + "0x423d1542697d3b737151c1E7fFeC5C82dd5b4B12", + "0xF2978B5dABb93964410FB0394c07f144706a2DF7", + "0xb2Ae7773e14d857D1202cfc2A6A5a2A3de1525F5", + "0x6903E7e1B1d0b67a6Cddedd59d4402eEe2be3797", + "0x5275CC5a4Bb7Fa74f602F7Ed90b1Bc3579674FF9", + "0xDf46bfA73936458e4BC90E3d1A1CB60Af7e55672", + "0x12fDD8f4EdD6dF194B0b974F21B1195Ceee074FD", + "0x219EE0C53D8d8CdA8700647b1e2a58896a4Dc5f1", + "0x82D1883ca96e57773429E785F195E32783B1C246", + "0x9CE6E6B60C894d1DF9BC3D9D6cC969b79FB176B7", + "0x836e71f16A53577eDd2Ea377A8aB29A90ae0cDc0", + "0xb305466DFf982283dB8886d8b2B54adf88A1c467", + "0x3B1fc9653f03789BC399fC343d8D5B8fC1520EA0", + "0xd3d5458C07B655Cd79d4814A52f42EB8Fc59c24c", + "0x6a5334E6c9352C1b5735969DD439e41BCF9b4dA4", + "0x623331FD48abca0AF5457BA39b8328979C7FABd0", + "0x38476bc1deb12F317cabC3F49fBBb80FbaB6f3ec", + "0x12e610AE4C0871444C0539fc7563C5D3317C615c", + "0x76c2CD1b8A249F465F8445c455CB78E0eeD36f1E", + "0x7A9664538A303dB562668BA8458ef283f4323019", + "0x9e17eD28b590d0b422552b961AA685Ce230aE931", + "0x03E236ad091A944b04e0311d329161C2A15d6012", + "0x14da174FaB1356466aF2dD0ecdF94204E828A732", + "0x69FF5358fAe4bcc2ec6db6CdB30D43d129291709", + "0x5e53Ce97fCd3AbE46F1aB45abB3e9A9C5e19F193", + "0x304B3D778ADa6D14387dba400d48eC5BB9EB689b", + "0xF91B7e024b16468f45f53278609f1B13eBbD5306", + "0xc0A674041C3CcB2DD8E056567Bcd6a14314b63c4", + "0x2f374d4Cb2D5aA194c8279D6607D5EBD59493C3a", + "0x8577F44aB55c0b1167031eD26C20a4DbE26868dd", + "0xAb99C070540CE0aa4C7B978584A6edFcA5af37CE", + "0xF75db6544CcfC7fb5A9dF01B9A3Eed050562d0d9", + "0xe2D6AA050C655a3F0f95eFD8F2107778090308aE", + "0x8056dc8F2Fc8962A2A639651b3a969bB6223010d", + "0xbE782eC98CE7135A056E31300a9831fA35D52865", + "0xA7cFcE25AA5D9752C8ef5a86D5D76fFD7FAF16F3", + "0x896d723154eca634E5DD8A5D3EE76A4F1E9bE22E", + "0x7d0A940d81cE63cE716a1e3CD7372F78F79d2414", + "0x25E7Fc427E81E2819FA9c455131c1f857e42A81f", + "0xA2863FCCDD9Ef5D378374eA6A6DB5708654f0056", + "0xe9f6B6Bb8cA582e0E5DcBB03E95b247843fDCF2F", + "0xE126d84a1c2018b5F5ff821C1539eB8c53F27bF2", + "0x1B47235150bCCcD6B51F60F175174dD2d579Bc13", + "0x01D05B80aDf3a8D855a2560a7d89EC32A9F037BC", + "0x49FC60bC9c4420c8af344966E50b6f1B4786910a", + "0x072e333E4C5B434A59e74f8876035CF81E5f248e", + "0x8BbeCD49c2b44c2eb8A27E8a42f49B09cc9f0F31", + "0x36C4C53bB6b0B8C59E98b35a1EABb5d43B5bC27F", + "0xe2Ff239cbec4db2bb8273b4e7c4F1E531794d2b8", + "0x332df51FA1D8A2e83BCbd1069b771D9B632506E8", + "0x0aa14f7De21E9c8EE836D2aa73aeC761B34b18A7", + "0x414D5DE6fAF79b80069212efa523C9F8e825702F", + "0x18A428CE0c31584a21CF8ceFDb8849d8013E1994", + "0xA5504BCB6cC0492344151EBee943C62d2B2c071f", + "0xFf9E5E8cB270d80c2Eb6059c6B0D70a9aec96922", + "0xAcb17fC73Cf202EBe532E107cEc195d50Dee61DD", + "0x9800F669F13c4957C4e11f9C4Ee2678DEdF7929b", + "0xD6AB3210442765D0BD806Db6643Da28d307eb422", + "0xe1ce761AcA1cf320D3185b6eB37D58A81FcDaE2a", + "0x64368FAce21029A646d823c0AFcBB4dA08Ce9b78", + "0xeFF753B70b34ea0DB977ecd27C670Bf3FDa68EED", + "0xa0E730ae29d4DdbC39C40F94d799b75C3E17Bd58", + "0x54f18b69698D9310E8c645d2b498715907a6ec6E", + "0x043f1d06806Bb1de6aCAFf98B078dA1a6Aea55E5", + "0x1A6e210DCdEF318CA8c11AaaAb7e305E49Eb0e7D", + "0xd1779622B76824F952bF89b3bc009F6dB19BE701", + "0x2D94Dc4D69DB8eCDA720406C118702c0719b4345", + "0xF0FcB5B2eC2D511C4338F59528BaAFDaa1f3ff37", + "0x56c1589B0b6899B9D6e9334e98C9d16d8082cd91", + "0xbEC85812e620b56525681312B12eFCe711A58135", + "0xE49f9C282fE9C9BEc52905Cf11fBB93a59568Fd9", + "0xb9d1A4528EcE05D7451A1E8f42F60c93a390e350", + "0xe0Ba6c4c82376c76386D1Ca2ae2516Ab18882e3C", + "0xbFdd62139F97163D83Cf5CB7F940564b56c6fA5D", + "0x3523481702186C04740e50396D0e1C807cE36C57", + "0x25DA992fd0f68296a2659B69f1642F0d55360993", + "0x3587B15f7865D4F3F5cA15D29d197bB2f1E6309d", + "0x8a70b9FeFd03D8C54877bc26203A87f253266FC8", + "0x592e1E16016C25651513591B7eBed5ca803ad39e", + "0xCd710d5Bfb7A7e85dd19fcf0470eb24Ffa057858", + "0x0fDf1Ec1F12dbbF56A8b066985B1349cbfec262c", + "0x0507B3ffe3836aD0f256f3D53b8fa7653e54E2EB", + "0x3d7f42a6bc03Ed9dd42b9853EA027EB5c844A36b", + "0x7274a097d392a564622fA8569599D04fF30825db", + "0xa84842F23737367274B6949e61f0bA8f3239b0Bf", + "0x0428fdC1f59747b811Eb3834344e065c216241C8", + "0x3Dc56e85283cA780d5FBda7337B6BC98a5E4f22f", + "0xD126C57B7D3D03F2280dB685664E9dDc30300146", + "0x119030aD7E6FEa8f694782898038da2E768cF6BF", + "0x99e4b3a0217f7e45815CC7726CaEeC63689a016F", + "0x875733f4C30BFF1fa1ECC78045CEE4B8a15FF08E", + "0xA6554D85b945Ce01505005bb07e56e6126259686", + "0xa9E839484195Fd7685374f29D67f68590c8B2bDc", + "0x340209C8975508A8B6750C01Ae290Db038a275c5", + "0x9f72b48ec2410586a49c75963F094B23738a0CF4", + "0xADDADf3Df10bebFFf7201427d50Cc0448e2E6F3a", + "0x8f2b60Bb82E11fA9539bbf88803EC079d25f1B86", + "0x096eBBd5FdC3B1028Ebf2279820028c0272412dE", + "0xa3acCb1FAcB57a66720B1873466855337B6d5A20", + "0x0d3409eEd02927d6298ab98BC79496CbDdecf0B7", + "0xA6A2D935363669EA08723798284bc3Fcdc58f9Cd", + "0x6A16Ba7B1653d19802d40b45BA4aDb74170770C1", + "0xC8A7403AC82432c8eE1f5eF998874ff3d185573B", + "0xc57684e4bFD9fd5227Dc1C66BA03CB9592f4e492", + "0x3e6EdBa22D9cE22a7168406d26598AE20FE08e41", + "0x86Ae657b0b38429E8072523F459e572F777cA276", + "0xCd93Fc667df20D3ed9B0b80f23a1BF415A9085fd", + "0xfF9c6e3aCc07792649644E97e358EC60Ad22b018", + "0xd699571A57D3Efe7c50369Fb5350448FA1ad246E", + "0x07Eac6Dc2AF876d2F0402f6804cc6cEAa0cd20Cd", + "0x078b7b9520514eEc7e767574e5F0BF33C84d910a", + "0xbE026AE67432eFa9aD85b897A70e0BaB0Dcb1CEA", + "0x8E29D0E2CA8e92a9f27192616E2E9f170FD2A035", + "0xD522d275F7b178DCb949f96e5Dcd73f6995760e1", + "0x6f199F00655e0D3e9c1f2B654570758CA1e33759", + "0x465FE8f2122966769b3d9D1E7391Edf0ac08CB43", + "0xe9c2F269ac049c6D2C3f893EB5465F8B33E561FC", + "0x5fD3140Cbe45241b8eD6e7c4648c2AA8068791F1", + "0xA0b427aAF79BAc6031A2cD2b7BaBE1D4f827aA04", + "0xcc731095cD449EABe6b02a313d78Fd65F266CC29", + "0x6F85f16B35a6BCDe77Ff085634875CA608c826B4", + "0xc07686AF7aEe9353D8E88febfbD72ec68411ccCd", + "0xE43fA6CBb7DdA0bD863593c5E75fAe9B4dCD5CD4", + "0x29d112c777Fa76d1c404502f0e3Fdd7B7662e1aA", + "0xdBF1d832d1E66Ce1D8498D578364C0D7D0614C7B", + "0x9550bF7dF4065A0a0d57340b603478cAD4c5d465", + "0x08d97F4c2f0225058D096F7f6B637007EFeE2880", + "0xc87C7A63FcE82e5929524EEbFf820A16FDcA1147", + "0x96501927163C32ba990d0Cbe6f42593c88460Af5", + "0xDCB25ef40Ad51bEBc2e63D34E5A15E21f85D212f", + "0xF93ea91d48E3fCb4A9D710c6493b4F6BCcBE4943", + "0xcBd9f0c0639FC515ef64835354935Ef14FBF48a4", + "0x92656aD77da2e82731d3cc083116960Ce29DfD74", + "0x1338FeF4210fc6095226fb6388077c66f153087c", + "0xcac88c05AcA7456FAcA8a4f0a472ED00a2136D9e", + "0x7D9F51490414a144a143F31fEdc434Fd96052AF9", + "0x527465920dE22AD83bAE0043f0bf975D52C9C63c", + "0x44bD6FD314dd5AaE5100f7A9932575207fbc3bd2", + "0xF712aB8c4505eB4B36a2Ff9Fa746470be81E6538", + "0xe4b7FBC0c55299Db43553908C36C98AFd641D3cF", + "0xbDec7287C08C0c153F9843C4796D59e3BF7178E4", + "0x60Ac0b2f9760b24CcD0C6b03d2b9f2E19c283FF9", + "0xB5d64a8720E8dd600AA30965cFA14799DF6824f5", + "0x32066C25EcE53F6844b35C8Ff155DE36b40550D2", + "0xbCa701b4FfA41a73B483ebd1072E8C75BD4661eD", + "0x27F0D8Cced5F180a330EAD4Cd11D4aCB1e42FCea", + "0x28B6182569e04234Ef77bA48869dea352933d168", + "0x61545187D0d2DCeec9261f9c358CAB14AC8A4C0f", + "0x19246d4cB92671D0F2166D00C9de687Ead3206F8", + "0x00aB97c782c661fb3D08C96dce06176938101211", + "0x3096D872E1FCc96e5E55F43411971d49bB137B9B", + "0x4291ab8a26EFdf45a89F1b4bA2aaD8229A778a9b", + "0xf291280602A6Dbbdbb08D48557c41Ab4C45F8b49", + "0xD59586b52f6F8c90C04c2B9718bE7b2ac7D6Cc61", + "0x9acE991de0bD8F2E32a357024df85B40283319f2", + "0x96A6f29ca861B7420c586E070Fbc767E357BA821", + "0x75AEC1D6Bdb2d63BeCFd6c65A01f3E2175B98A62", + "0xD7620731C75B0F4078707be8F1Fd6B6b7b277A3E", + "0xC817EFf12fD883b20b9Fd91dF00B029390EC1FFB", + "0x33d835ba506684273A1d1D07F2F64d63de389150", + "0x6b6cA48DA27304eeEb1D15d355D948f68b852733", + "0xF0DE256d741910BBe86173f02c795E1034CdA0DB", + "0xacFb5D441F92339218Ce5B34A794278001dE6073", + "0x15Df788d6a0253B91cc2694ee6665420247E381E", + "0xbD11f25ed4407b289bDD27D0668435A3493a4E02", + "0x30F89f5f4cF25f457B4D9cBFb64a64A97F00bED0", + "0x28DA3DdE285D8F1f87B2D858f89961Bb8B9Af180", + "0x9695b507A59f00f2fd667525D9Fa661Ef0280308", + "0x9a499155E6D5F6A8722876a5AD0c4A47b3322C87", + "0xec04e0a492f5A5fe631AbB4c7dddAE8A2c84e57E", + "0x765E9AF7210c2d120096cE5b5709e8060B710474", + "0x5DA318b6F25497bEeb5FF3783582A01D27623CCC", + "0x0F45156F109e474295913D78036FA213b1745D5A", + "0xEB4576fE753DAB07635c0Bb6c8f0A355e1Db5d31", + "0xd210ae3Ea4bB169163F4e9876109A2C204Ad3a87", + "0xBB8aDE1A31A79422B425e876184724c209f78289", + "0xF6ccF8751fd02DE2e14cDf0Ff0Aa36D6e7A1aB28", + "0x3a28F433F23E17a6201552b5076869Eea077b6f5", + "0x3692854Ae1cE9a91Aa0Db6888105B466C767e112", + "0xe11925DdDdEd86a78552ddF7100b451CF22743b4", + "0xe601043c33d78EAEBc8e27e0436e7A0d11efe1B0", + "0x24046EC223b7a42EABb0527D60E0138922bb933D", + "0x9C94287160B7FCD729aD6c582440052a949Ee96A", + "0x353D566AF2B571F6Ade5Cc9F7a91422f6f738098", + "0xd9a62b4938ccec820445009Ab91b71cd1fC6CFF2", + "0xbefF46F500c3Dd2A6301b9D869fC49B47302327c", + "0x9a483103327b3b0814ABCBf61C4860C48fd84f07", + "0x36D6c29e957FA8E533F1096711CC8D70931F101f", + "0x6512A1C7D144aCa84fF06dF1e70b84Cd8186a949", + "0xbEda49BdBDb8aBDD6993B8059A8192147FBB55aA", + "0xe0A86AFB73c0922A50D4A985A25507EcDA8a9B51", + "0x2E792c5e7827e8b6BB26761E2C4c44B867838166", + "0xB44647814dB8aDa1b67E8240938d2A6bf77184Ba", + "0xB83eB831f49c4fC41Ae14851b16820a233eC0cEa", + "0x0ff51f912EEa28b6195C2db43e0e90Ba905B5b42", + "0xD996DdB8b48e3728206fBF0bfc91b4378d6C0C51", + "0xcF1Cf25e5CD7aEA5f86D8400d600411BE1d85D90", + "0x99AE3F791EB80db2a96a1885793c5004273cC7Af", + "0x73cCad884e29ef092a7E8B3e943b91Fb3881a7D9", + "0x006FE810cb3830872bcB217dFFf18Ebf1E33B290", + "0xE2976f612E6220643CF23f02735e7c7b946da315", + "0xed933b393Bcd6f138B3843f5bCD50E3ab6E673F8", + "0x173EDA92A4e3F1B8eDeEB02880787b36736eD937", + "0x5Fe3661Ad3Af1A2Aa85001364B988Cf628C38811", + "0x789498eE77215b80d201A814228fA7f248c3be18", + "0xBAE6379ACaEe0FD0d5922CABa1895292533B6FB9", + "0x6fb5421989807f828926b03E44A5Ce61D6906EE3", + "0x1e54DEb2D2176f715f719d328Ee35f06029236AD", + "0x459E634907E0722c7145EF56765FbCD924e86303", + "0xF0eeE1c6CD279Aa9F8c87e8BEfe0b688440db1c7", + "0x2EAa6Ac9dc11B1C4700443f52FBc9d067C1D0D65", + "0xeD39c5e2D4B2Fd31E0F9a62fa5416BAb8C062a3c", + "0xC766545EF68b49E5C70F4E04C52a9caf27b9A9A6", + "0xf2c9c2EbaEBfC38f76d825b09A91AEEC477F4fcF", + "0xCC2e601F4e686366B7a0a581EaE4D26b0768F4b2", + "0xcfAACE98e68CA2E796ce396760515606fc6855AE", + "0xc19eD1FA9FA3968FE12e97Aa4d511aB6bCE1320F", + "0xfc3267Cc1eAD34059f43A70Eda456b29D1D64214", + "0x9aaED9Fbf4dAB79296190817b85645D3c9DcbE7C", + "0x5b79264F6C65AA1f6B447AecDfFdBFeB8d71c18b", + "0xCF2C2fdc9A5A6fa2fc237DC3f5D14dd9b49F66A3", + "0x65877BE34c0c3C3A317d97028FD91bd261410026", + "0xAd24D3708240240D3F141580522B13F01e15AA9f", + "0xa380dacE2095Adc258d2bE066e2A60a9DBacF7aD", + "0xF2ba2EAD3D26C213721114a02E0C56D8631AE388", + "0x8Ff6FB658244BDF66c051fA54DB6768Aba552E87", + "0x330C17CCD25839DC9Aff158A30Ce42959D249EA6", + "0xFECEE8E7642A25E6A07E0F5eF4C09cd9557F6BB6", + "0xd6339759848E5CEf0a90fb6bc3406A62315739c6", + "0xEe700cfA2324CD9c06be498cc65DE24b6c3D2893", + "0x1aD4A9C4Ca9c0A6b1A066dB8447873120F2FF9dd", + "0x48BE343c86e5D2233e874B23Dbf123B0C2f80857", + "0xD65647E84C4eDc9A0c9e14470Fca64CD2Da8B35e", + "0x5309a0f7E9F92f535009bA68172b01806d223A43", + "0x9D1F79D20b9327B54096CA287f314a2e8B72Be36", + "0xC8Bf09987727D53DEffaf02B2BB1b223Ca014bf6", + "0xB050e0b02736873BDCB09F1b289aFA79744ef33b", + "0x54207Bdd0b6b1DAA5Bd98c0c8c2EBDEf65f63a3B", + "0x42A82dE905239E0c5e584e0C80d7a15825B9c190", + "0xF62bdAFc4cB3808346c5854c0Fd129F2a22c02E0", + "0x5fDdAF29681F3aCD3ee5D88Ef150F066b4db9890", + "0x73dd0F5b465b57570156F25907Fac0f38b05aD0b", + "0xA7Eef766B1f3F3d31A0c7C6Ebd0B3f31e9a87634", + "0x069e85D4F1010DD961897dC8C095FBB5FF297434", + "0x4A7E2122685eEa970ee0F5F257c62400d345cAfA", + "0xDD30dad7a3EfaC0C849e5c949dC1fe6B233a3FA2", + "0xbD65E3E2D82430Ff414685cd5b4DdaDFdC8b0686", + "0x0e90CaA1DDF2184bfDE42Cb7daEe3642F9fF54E4", + "0x0d7dc488F8276aC1e651D982ae6A16dFf089D813", + "0xF429fBF67A42576fA6d38C310072463e79e2936e", + "0x197B7DfeE0EDbDa94d8e718378dfe569E9C22d42", + "0xcFe5D190Bd42Fca25c675CD0910CAf1e985BAF93", + "0xBbFbFF2FEa1950d404742DeB24ECe126f3796506", + "0x71535AAe1B6C0c51Db317B54d5eEe72d1ab843c1", + "0x4413A906a012F06bD0ABDa5eeC45F7D67b449284", + "0xFC9e0e4F0359e0972C02b840B9c36E129f82ab25", + "0x4564d6d107a19d3AB3D734A7BaE61Eb63dC3d30f", + "0xE773558E6b7C2c27c35550b29D1d337151321986", + "0xcc112a107ABe98901B9EfD9ac4880A143dF48083", + "0xb3A81311c3A178928cf96F5092d627a259dc5aD5", + "0x4334703B0B74E2045926f82F4158A103fCE1Df4f", + "0xEe650F5599Bd48F27b29B254A4C45322A755C6b4", + "0xeD02383d3B21052A02EDa1b456f0F154738e7a2D", + "0x83cD4F9d40aF885DEc803e0cAD2156246110ABF7", + "0xf0665d14A7d67c7A08122aED9901cD45fE0D2b79", + "0xce3e142Bc70F5a0f674A2ef5649b3248842cf1A5", + "0x9125b2457479964540a0557e3B010681317B635E", + "0xaDc92E52414bB1A407f6216230bE1fF574f7042a", + "0x7a69D06fd825fA64ca81cf8dC9710Abd7cf05C24", + "0x63d293a7F2ac5443cad758FB481aB32442d4259d", + "0xDaa8cb6AB1C9AffCC012A5A023EF1B79f91c6297", + "0x4f1Df0B690c00bD96B1E71C66cb4E75cb22Ef1d1", + "0x984F47Fec079d3349bb3AbDf05e40720E1C2d968", + "0xB0720A40d6335dF0aC90fF9e4b755217632Ca78C", + "0xD18B8d61c160e779ceFEb61d47600061007a6130", + "0x4367Cc270D68925d9bA89CAe2A9c2877f3acD69C", + "0xdF3f35BEd840A5ffDCe0fA383217a19A7C34E0B4", + "0xDA7Ece710c262a825b4d255e50f139FA17Fd401F", + "0xf28cD05A4d9C5dAb90C99983c996343F0DD006e2", + "0x2ec9566FEDe31eF04D9A5d4fF9DBEAeab50C5273", + "0x68CDB7D616a0a2703044bdBC1fAc5E03167aE7b0", + "0x7c895f59b8eB7Fb88ce41bF03156650Ac40E16f6", + "0x0c6a0b7Dd1063ae7443AB562622bdB4603dddAec", + "0xFa4E885E98aEBF061bCC90F6C85880b45e5783ee", + "0x0403FfEd505E4fdF9bb13309BeC69EaA803b06e6", + "0x9aD08e2742696174e1277f7710568A2724eFa82d", + "0x2Fe95bA46D36e6Bbc8d3bbC6BF3F18c85C91f3aF", + "0x5796F85DDE9ABfC2bBB28d086DEE994D970a4561", + "0x259Af9A30cd456D0D614e60BEb54E15824A85a1B", + "0x1125917201ed36700F86c3CecEC8C5DAfAe280D1", + "0x56Ae42256F47d488F74efe18089ec1E83f0ab9d8", + "0x297b84479d7A6Fa254E13fF8aF9a39A7b5d4D629", + "0xc8f64d227c715aE078D642C0e3a15D44FE7a48B1", + "0x43DfcD47e7c5C0059840883D9ADDE4149C8C30FF", + "0x9eE6f42531aDaC0bD443756F0120c6aEed354115", + "0xe5Fae1A033AD8cb1355E8F19811380AfD15B8bBa", + "0x1406a69a6abD0A3D904876724b20454B3EcA5eeD", + "0xc2dc013edD48afDcd7b872D51d55ccd1A7717e28", + "0xA4A62f7127C644AAEf733029E0D052E411fdafb4", + "0xa7ef228999570C8E507E6ce7D7bdB12fe053b7a6", + "0x716410bFb864A494dF9b943a63E942E6E895d21c", + "0x825FDbceeE8856cb55EFd3b0128ABf386eCe2315", + "0x802cd7AB8E2c7a8e1906063a707f3FF8004AC748", + "0x38B0d0d924E0A060296CC8B39f436Ab6b92745F1", + "0xc655B790FF812109c8F6c3f24fd20b3495164A51", + "0xb02dc413e90867f035Fa6d29fb54A1E9aB7b274b", + "0x77eeaf07C050a690f9B3C2E8e7642Cc3CBEcEEb4", + "0x01650a012B91e32A077135Bf7C9881C44b9775a5", + "0x5D824ac4b69776f5F3C5961043D684FF6091fAa4", + "0xFB0873956e94433B4b4AD4f07d6c1fE638F870B9", + "0xD583F58E120600961e3e44dEF13718fCC1C1f491", + "0xc02cA57CE4Fd197Bd34877791be417B03Bccc049", + "0xf1Bfc5760C1Aeb6Bdb16252142429107D4Ae0997", + "0xC677E71B02adc94534De993A907352f748d21143", + "0xd511A8D9448Dc95e774000dcc342207144aA9377", + "0x9Cac725F17d5deB4a65922a6043eAc4081aC6Bc4", + "0x9400e3b00AD9a7BbedD6912cf92f8fE34adc00bB", + "0x104f3DC171f5CE1d1501A49E664bA1fc30252Dc5", + "0x5A924579Ee6d0b8F8172c944A5770625B11b635a", + "0x42e4D76f5b78eAb5fc07098FE4B5c35102b5f4a6", + "0x643e1004dCb9B37d00442438f69D306473fDf58D", + "0x8131f8bC771733D44500CC35464fb9d451932269", + "0x4355Ea5F88d29EDb19c8D5861b11CAB5C0F7CDB5", + "0xabC3B8A22B766b9C5Bbe73724D2F35f81359e72a", + "0xF28dCe94A86f64606bb80B38182C2962157BB2Ca", + "0xc3A4fB7Ee898f675bd691c05F43aD5A67aEFa81C", + "0x62058F928E0b8af1279670437d7dFD9E55e34560", + "0x798ed654F4D9599B99f94F4A61609b7A7cFBD200", + "0x610BB423d080Dbbb9eA5fb8BAb100AAd2D81BCA4", + "0xE60e0A75Afef85809763d703c958F101bF7B5C45", + "0x47C89b9a82a508cD5906D24d613EcC755cF4BD41", + "0x61d7ecE32854CaF46c6304Fd536704EacD4dA256", + "0x5A9F254F6464C6BD721c0847C69612B603f09673", + "0x4cDd6Ca7dA844AAA5bE3c7BD5b9CbD213c77F6c3", + "0x4BDFAE7d2A6FC206c5a80f4d41e683a5D5b30D5b", + "0x85eD40DbCa94B5BC73c6C7EC7f4eCaaD04A03a22", + "0x59dfe67119FE222F4a5485B15C6F0B4644e43E8e", + "0xA22fFcb5295C1bEB7238E3914E19e0bC18Db8f23", + "0xe766cA34C2600FB9db878d6C916e8A8B6f445e8F", + "0x5c533d8bF5f06a36fA8996C81fCCc30FBD04A9FC", + "0xd60a9bb677B43820D4D3E8C132d1EC60dD1d0313", + "0xacA3bE620ab0378F551531fCE89006A358b84EDA", + "0xA529DbF959fc80F9Db66d990b47Eb33e26c416a0", + "0xC8A44E444e7dAc7744bf2FA35256F7300e9c04FB", + "0xAdffC760EDF4f6146dce89022B5AE7EbB7edD2B9", + "0x708B1eBc569D0B4D127fFd2aC5554EFEC032f0aa", + "0xE476DEC76e2cc238633d1e361b5A7155F46099a3", + "0x2E15D7AA0650dE1009710FDd45C3468d75AE1392", + "0x9763bC90dd4E1544da011833aD1Dd25b8C0EeA96", + "0x63F19569f0f729F137e1042639Bf867eAF8E7b18", + "0xA6e14452Bac0d4CB25F788f1DE0AF7DECA87f514", + "0x92b39b14AA07C309317Ea0c1D5A76B2808Ef0ae4", + "0xbfFeC9dB5D96F5F8367c3c8E95C25eb8B7d44AD6", + "0x93e961b3A375C1831db9C3cE604d4b54F731B4d4", + "0x14fd7D24eBF81196dfd6d3AF740D5024071859f8", + "0xbe7DD82FACD47D3598803F44CB980078466aEBA3", + "0x17561E4A5E16E619e049E3A61A64F2A2564859f8", + "0x7f4437dD2881468525343b86ef4598d24a21bcAC", + "0xdA90a6e5b488a617C49a412A198b8187b7e9c999", + "0x7b83FEbc8cD693eeb15bED36223DE459B0ABae04", + "0x5eADDA0D4C686978982a633fb06910F5a2FccfbE", + "0x0e011CF6a91dC8C3CdF693b0F8f214760dB1Fc8A", + "0xd79C597a5d5e4b093c4DE8DcA8B9c7927d0d4B68", + "0x27107DD841eA4Fd7535C9f7905BE3a5d0CAD374D", + "0x841382062d00f024A7a4Fee09e42e08DF41dD714", + "0x48E87c81A1aeCe2a39B4FAcD8Efdc3FcD63c636c", + "0xD79B646031aE71f77DE7a50CC5F189f5c62b7A5d", + "0x989593A779906A51596e84665E24fe2ef4b1F3B0", + "0xFaa41900848b7f68F4859676432811E6fC46a15c", + "0x4c807964532Ff73684740000F891230417EFf375", + "0xed84906A819e3Fb51C0b62Bb7D7f86262888BfF1", + "0x714Be2AAfd387bF46cF5b9D48d596A2B851e5681", + "0x1D38DD297F253837eF2b763BA9fE2EeCe45c48e4", + "0x22f851bCF7353d072Ed5Cce3cAff096195D2794e", + "0xEbECc0a1eB0A05f3cE7100f20Ec6b97cb93F0965", + "0x843fb684D6767473965BeC92C211C6De690f4B27", + "0x5882aa5d97391Af0889dd4d16C3194e96A7Abe00", + "0x8f35Df47832dC2015011D0af1D5F1aBd6f7a905f", + "0xe1dA9E3ea9eFc074EbFfD4D2bed209b370705188", + "0x51F9fAE0199f65445c9C3d2429c1A5672ce5B226", + "0x4463e9a525b0A851FAa6398b16756F43e64A36c3", + "0x5E42Cf1d478F52458885f75df041C6246ab272DE", + "0x7682161c9fbe3A4065B38c2CD3ca61263Fe27769", + "0x2C42346820795917e7D11579Affa976b5a571573", + "0x1f92b5affD12981Ef0FA7Ba22a802379Fd36929E", + "0x44a787f30b7b779315e13843A877284A708b879E", + "0x271742440a1f6Bd36dE80b8F7900D1ed7DADF3E3", + "0xb4900E989AdB821a2989dFb6171AF6Af225d0Ed3", + "0xf389994cfE868F531c09A9f27Ca9470620d8686e", + "0x524C78a7934e56216C4AA20dDC14340127308205", + "0x72C5cd18A51D53Db34072546A7A38a4f73600d92", + "0xa872a8b67FA8047265ac4bd38022107b7C30F85d", + "0x5C0cb1Fbfed1cf91eFAd1Cb3f6DdD55f33665fC3", + "0xCfFC127C4f491856cC93433cC5c61a18A6DEF6Ad", + "0xE939d186c9dA1F3640a8A53A3df9deb643021Ffe", + "0xF4f58044B4AfBAA53BfFD87f2dCe77a5f0B32a64", + "0x6d0BBe84eBa47434a0004fc65797B87eF1C913b7", + "0x0b36518F2dA8c357f235bE18b0B28392B45cD1Ce", + "0x2637652AC58aDDfb67b401a1beFb5f48b967574D", + "0x4444AAAACDBa5580282365e25b16309Bd770ce4a", + "0x4b4F333E18dcC441D006ebd9bcbF5eF39D7CA1Ce", + "0xa659aCBd4Cc99a321d44740182B6A8B9264b0167", + "0x0b99363648eFEa66689d58a553bb015957083C57", + "0x64D4B9B24DC4AfB5d3084d7dfb02d5602064E6e6", + "0xB9095B98BBD01A93C395cB2e48E4cC256D05EF8B", + "0xB5d47d2E05401C21303903fDA94285A05058808E", + "0x02cE473377B650b1188778A7e75cd4b31F59D8Ac", + "0xC0670E962dc0feC74199E216eab3252D98743612", + "0xbe7019f8333b0B3Cfc707ac353Ebc2123140134c", + "0x68C3cb93A893A893953d654FAA9D2DC15edCcdcf", + "0xCC9799C6933425D68687bB15c817aF0A667fAD87", + "0x8E900D7CC784A47b2f5B4baf0871aaDf792D0cC1", + "0xfd5F1397e32506AaDAE56514bCF6cE4246EcdA0c", + "0x19dfdc194Bb5CF599af78B1967dbb3783c590720", + "0x3C01182c3BD27Df6A200dDF0059D39cC21c3434d", + "0x9aCf8D0315094d33Aa6875B673EB126483C3A2c0", + "0xc7B8e515C5f0Fb34D301313b636b29d866A2e69f", + "0x6Cb396E01f3aFE12E04B1097AC88f20ebCD8D711", + "0x14ed950929C02124eE464e930070614a65316fDB", + "0x4aD8c6d982EA20C6b105C480C866FEf79dDe36dB", + "0xcDC95322054561DdE9B80c2E5A22f8E09BCB80e7", + "0xC00a6614B8030cE216302016e11C6B4140B5e499", + "0x18e8b8Fb3c843121B24e4fE7672fbc46C3c50631", + "0x70a13e91621a580bc6773e8D27DfF896Ba6d76D3", + "0xd6f8f76EF075e9B585a847B730C0531C1354671A", + "0xB5997B4fD73Dbd007119FcE4e24Ed552C1C4cb99", + "0x574C5a477851536D8675A3aA5C76305C8b6ca2a1", + "0xaC963B7ae5C75f263809C0D8635aC183dE9A2a37", + "0xEbc8c2E0b8C4040e18EAbaB8459621FAf1F6513C", + "0x4836bE8C132D02DaE41f3C609b109272f5ec1AA8", + "0x897b5561Bb4e815b486952391fD2e27C648f6D32", + "0xe27597Af91352537f4EA74622B5a2BA6d464d9EE", + "0x119b134E974e20Df79feA161B08970025e4D5325", + "0x3c9eE43b96bd5D3A7060aCE8c98d75736a1EBC67", + "0x2EA2ce595Bd68a0CD1FD34b06dF98970952Fe4D2", + "0x1C6E47D1EFf818156781D8C0155153CcfF2E1aEd", + "0x0EcA5Bd7b78603C3A4B3292914C98b94500f843A", + "0x509dcC51971b5b77B91585553F3e7C2527c92460", + "0xBF88D8258202F26e92acf7f0349c12c6AcAf4199", + "0x08b68eB975a124a895ba94AeA12AFFf4657f4dFa", + "0x544a40955bA1C7e56E161a59E1319e3313C25251", + "0x6547835644105F1313FFd5194f467720837b0c8C", + "0x7A78Ee74D3FA77Eebf56797f9bB3Bf06b6878154", + "0x103a1F94Bc38313Af8724777Da71201e40ADeF5a", + "0x8a42c32e61242B8c7a69C34762F35E5142a016f8", + "0x5d139aC18DF75fBEBe6d21dcCF47dc54CAd74F7f", + "0xDd1aBFA095818D4a26C02E8Da45CaA5291526aDa", + "0x0724Eb4BEC0bA47CFb808EdC582eebe79B46B508", + "0x939714d0FecF95D77d068838910cb6ce921a08A5", + "0x2d03391bea073095EaCC4785aAD7e63007603dD5", + "0x60ae40235C11116fB74a1ecdd8A175B4f955A1d5", + "0x510C5E29E7d7ed78D4A7D81D98BFaD8A2a67B45D", + "0xc079acf6caDdaa3Ab905C5Fb54330C937D19F60B", + "0xd94AE278E5907aa734Bb895A3729AEF074103891", + "0xD8EF72f4F03615d94993C4a826eB4deDA1F9d645", + "0xa14a711E5D6F38bCCF81d703cb8a949f9A1D3cBB", + "0x8C1f48a0DB29aeaB0E0Ca7214C92cf6CCe877279", + "0xFBAb220C53a3e4B45f7275d38139d3EB3E3ff8F9", + "0x8af7810012012Ff02f0734D46d09Ec1dd058cAe8", + "0x615edf07705eD6b2D5Ac012f780236DF898aEA32", + "0xc5e81cF721038d636fD089160a1aF5575130fD16", + "0xbF1Bc5964f7d035f83F159F00209ff4352BD7394", + "0xABd9221a59ba5c03FdD655834F07F44818979698", + "0x99b25F2B6A5CcB8dc45EA07041F6CAF045D8eda5", + "0x28fd174fC197ba46436D2Ae835d74934e57c3033", + "0xF17ac82CB2c92853100f150591973857B1b48D7F", + "0x3838c954D0629918578847378Ee22e6778473239", + "0xd8855738cF546C6950Fe0AbFa72cd4A98103f0C2", + "0x501F4860aAD23FfA53992DCA8316952e374EAEAB", + "0x22f6D1997f614aC2a9CE43dafcC92B24A9D97b7E", + "0xC57ec17511BdcEF72947CAcA8455127aA751c1B2", + "0x9Fb470376F12AF151c9ce88bf7b4acaa13bAd02B", + "0xe96664029ad91B2D9928bbcAB585450f78EfF8cc", + "0x95fE3708d68B83ba6A9ea6221d6f64838658EBA7", + "0x386D1AB0D50764e135b10247574264Bec20F3E03", + "0x04704EcaAb8f063b76a6f2Ff45D2f414F0aCaA68", + "0x11ee7dccC1Cf223318eFE308ca55C282F1d355aD", + "0xe6445ADFf4178693AB0AbE22ec1E5F44841d1E3F", + "0x6E88F95C51AEbDac258994490D133c95C871b10a", + "0x605088609f9AC2F1F4571C4Ed57a9318dd381F0a", + "0x90F15E09B8Fb5BC080B968170C638920Db3A3446", + "0xe1452baeA1eFb49489CF642021C86480965C9e03", + "0x06AD2f93B61eB355e6e27D9eb8C67EF8eCcfA50E", + "0x21B8d5ceBf0Df88a151c50bcC06465B5E99FCFdb", + "0xF59cDA0Eb96D6DdCD12480bC31c9ABb75eBf1Cc6", + "0xa28ac1264Ea4Dc2fcAe85C379A25E24B44A1aAe0", + "0xa24B12943dD600052bA36A2543f3598C5B6baF3B", + "0x6c2693F5a936f37eD03CfA8465bF2D8BEFf19A0f", + "0xC14352aD978771051F46A3c8eeBd43Add56A6899", + "0xCE8Fc6755EfCaf7F85C28901Bca4F4b936591542", + "0xBfA631508ceEF09dB02F762f7674bb871108Fc43", + "0xCe08958266f58b638527B224D8b7141a3ff9C77E", + "0xE3E7620168DF930e62fb8Fd04D6E8049892Eae1D", + "0xC3dcd2eb5D52fA4A870a69E63350Ecb1248066E0", + "0x7B84C9d300551AF54f1b415D5bF211f852725dd6", + "0x7E9d2Fa27745A7a61b5B433D6e413496B2Da9ee2", + "0xe4e8301296bD883515D539490105b1C797C52feC", + "0x0000000B4D325bB539676dAC6ec3413d5974cf0F", + "0x2b597e53BEF7a9275429a3d5443fCaF0Ab12C26A", + "0x015Cdff2E472F657E8c34B4e88fc60f16f7610ad", + "0xaF184b4cBc73A9Ca2F51c4a4d80eD67a2578E9F4", + "0x585491BDfe0e82edc877f7BA5f9294dcB267c093", + "0x49E59dE5DBF06ED83116AfAA0570Bfe13a8D5bA7", + "0xDAdeF83811243471424558e8d48b85bFcDdaBa2f", + "0x4FfDA342DB6366E564b28E98DCbc48890ae204Ac", + "0x21d9c1d2cDCB44b04e6DED64Dd89bcf5fcb09cE5", + "0xaf31Aa6248Fdb61427D221D16B8C7514F1425C2a", + "0xa762E2e666e70da57fB27d48DCF16b64535cB170", + "0xd09e3d8CaFE386B13792E08752E442669a571f4D", + "0x08D8aeBDE1A4FE3ec84678D4dBa286762518803a", + "0x523d0cd051E272856a2C925C08a5FBA8425c4944", + "0xE751D56c31DC1B16595a2CdfC94388687E26a674", + "0xD54d23200E2bf7cB7A7d6792a55DA1377A697799", + "0x5Cee202B51518b0E4675bbe129F6C9776F4417df", + "0x475E80062da060826A54B3e29776eA55FF4d32E0", + "0x9aD92F7f5EF6F4bFf83c2e10298A927f0Ad7952A", + "0x8d67D01Acd6fC40420F512C044BAeA0f0Da0056f", + "0x4267Ca7c40D73ccBB532fb0F2359eba7cF836112", + "0x09e63215D0D7B0A9D45E1d708A164A5e8C7d8176", + "0x3d80F82622B0Da34874fdbAeF5553a00ca31E623", + "0x804c3083d6565426a35482611a514948AbFb00Ac", + "0x0423275d2ac0A164B91cf8F14Bce45fd6898F563", + "0x5957145FAb56DFE5Af988A4F55EDa4B205922C1d", + "0xACD327490EEB54cc7d1C9c28e3B5A39F0e472be3", + "0xdE698282E354705D609c0BC9067174160b3dd5E2", + "0xC9F7113042615cE4796f8CBbfA6f42170D908e05", + "0xDbc03531e5cF492aA0F0668618B429858F0d3125", + "0x23F1045cA09E9c3206851336a2a05d35bBC812d3", + "0x2a4E02673c3a50C4D5Cb1C372a3654E465d933a3", + "0x8a46c80E07f946662E80c237a3Ce07bE60a42d61", + "0xBD2eAb5074658C0F3FF0eF7045563CafdA008bCB", + "0xB329f33D249662B0F2980F823435FdC7aec3EFdA", + "0x05a2e50C5E4d724897b67b708db432A38c985f83", + "0x149CA92A98E41DC669d673Bf925085567137B7A4", + "0x4a795509a521d22123A66317708698e81d147714", + "0x3967D78660bCBc95c625d58A40C42Ce10bd905D6", + "0xBcA4D68BE543dCEFb1a8bcCb519503f9ba3f2026", + "0x1709e403F22189a289fD9E9d43652d6ec92Fd2F9", + "0xA63ec0012Bc7334Ba5Cfc22901748BDDd854BeE7", + "0x6fCf4b90fd058aede5c1f37f84940c5d935f0369", + "0xa6910BBE603751e19A742154F0986efcf7c062a2", + "0x776Ef1f7692182426E98fd5F383e5ea10DB5e397", + "0xa0A8338295e07E11f118DDc8e3F604b84cDf791D", + "0x0FeB0f31D67ADBBc950Ac0c5266490A9a9735a49", + "0x352bF5e59A4C1a0E4403278fe5C9189F63AFDe3C", + "0x95A3D06fe7B39399E2edcC31435a235585FeC852", + "0xAF7a0afF1E68B9584C8f41093F9977C6E6297a13", + "0xbb1bf082b1a61131D3345c1f99DA32aF592Da2fB", + "0x15B1835430a1fFBd0dc652Aa828a8c2057A82CFc", + "0x8E789fFEec91C349c6699D3F3A4164e45f59C9CD", + "0x6D7fEA7c2A257048032290A8D4A4B3f966F67c1c", + "0xdCfCC77a2e690141432561a9746c125Bf6704380", + "0xddC60D163015CF5Ca1369D557A2ca1cC2de1F1e3", + "0x4242187941A1A2d528Ec591Cba6BCc4cd4BCBb5f", + "0x4809Ecb2fcC97477e86C0cD1499E9a102FAD3507", + "0x9a82aA6eDbC473a883B8906Bf41084c9c1B760F2", + "0x1A2B292661f0102C5961aFc42b278fE8c9169234", + "0x104BA5aB64e949E05Ea77d398DEB30d16c713B9d", + "0x97ee4eD562c7eD22F4Ff7dC3FC4A24B5F0B9627e", + "0xc046F4a055173832866C0010b15698baCCB7B7E2", + "0x31DE2673575c1b0973E628B8bc62Ebd0f4Bd886c", + "0x8c4EC20aa10b617Ea6C008bF7a94Ec97Cd643bBB", + "0x3b3e8C9e5ae7Fd7226f55e12c8F5198641f65aea", + "0x029fcD8016fc878A674ff8D7302a541FdA8945b7", + "0xFf3e84Ddd3e8809591c7ce584358A5A3f4aDcCD3", + "0xc12DE812ae612B6d514b52d529F97f6Acb524c8E", + "0x6fC63168323527883D7CAd0d6B4Bc51932c22ed3", + "0x1A0F3ff515f8a0d353eD7b0eED6292c7560d221E", + "0xB5231E4eFFC32aE4731D4c6ef65aC58E116c1EF6", + "0x952cF57D0518BC03019d397e06a5a9dFCfde2caA", + "0x7d0b45dBEf18ad95633375236336a9E7B9Af7d76", + "0x27597BDbc3C8B3E865e2CCeC81314802FD46f8B3", + "0x4774b0e8c77C06A3fB636243d63eb085B4F6150B", + "0x40908EE8c5739aD1eC1031B480Cd88372BBc5c84", + "0x0815e5B76416Dc593C5E82Ee7331402d7402a5B2", + "0xc2f44E7F63a23a1A3d75146425C3125Da3AE24AE", + "0xB4C93d0A04D4B7966776cC2A0Ba31b2C9Bd40d18", + "0xB118FD9852ec6762ed38799CB43F0Ad1f0d29a2e", + "0xb40c28F91Aa98A2234Ad9E5693FeE052cc173784", + "0xc766D73BE01Caa06834032bF5C8F8f0F41A7BF36", + "0xf6F441A3784FDbCFCaeFaEA6881571CA1eA625A2", + "0x88023c305b471649D326b7B95149146F4B5308b5", + "0xA72e1756426100c6207421471449E2Ba9A917e86", + "0xC6387E937Bcef8De3334f80EDC623275d42457ff", + "0x514c4BA193c698100DdC998F17F24bDF59c7b6fB", + "0x51406DF329a3E1145dA4da6d79143A3747eD51cF", + "0x88fE0288a14499b9A42B10fC031F91eaB341a5c6", + "0xB3bfB32977cFd6200AB9537E3703e501d8381c9B", + "0x94db83f9934adBBf5966A30e55b83e6635c745c5", + "0xF73Bd5710d3e3d4e8DB7EC6058F9fb77ff8414e9", + "0x1dA29Ca4E330F0Faa816edB6B55B46f9eE453F7f", + "0xe85FEa97723d9a67A9CB4Bc3E58FFDD6dC3a78af", + "0xf8E98A6539545590F70Ea8D46D5147118Ee139e6", + "0x51d18E5F6FA6534bB2fbAaf90FD6f3336327fd16", + "0x61E60af04805D7dDFB0CFDe0A96A3b1C15F3748F", + "0x5Be51ef6A7a4611c685c5CC40Da81C73A6588039", + "0x33b346911A75e8CADB1D3bfA1E9139db59396867", + "0x02a84163763Dd791328765b96aed43b6a915af48", + "0x609470c2f08FF626078bA64Ceb905d73b155089d", + "0x39F9bC86f9fD445403fe1698708E76E86078Ff23", + "0x88cE30F11723e38fFaFA91A22462e57222433467", + "0x395Ef2d7a5D499B62Ac479064B7eAa51ae823A22", + "0x99298b15b5dDA538C67F08af804D85e82450Ce39", + "0x14b8BFFEF5D546727DF9A4357e69c49907656179", + "0x2EEF4219055fd3C514Ba7b50316f1C3965A53751", + "0x821Be125973279e5ce5eACf2F9360dcaa9caD53E", + "0xA582da012A45FC26a61a134A720aBc913938eEd8", + "0x4F725e7CD53D76Af3DDE36Bd05DD06292A6F3c56", + "0x26c659E023E522264455374F82758E365994C420", + "0x04bAb032593c39C7d600DBB0e617136095Db47fb", + "0xbfA700eDc21a6Bf09d22Cea1BBA95FC54e7b6dc5", + "0xF09A845C973b8265c8519D503FA6097caaB7f932", + "0x73961ce0ac9220fBf71c86BD38daE888F75bB169", + "0x9cAb916493D31D1114a1D4F9a497aA886c1506E9", + "0xfF7D94d50aEdb1ECBa19A56F5A1EeD9371071eaA", + "0x4D3A307aEc8eEEA1ac093B1b2F4Dca8E23A064ca", + "0xC4Bc1d401b997c7855F4De64d197B41Cd2860aaC", + "0x67b9D361759015D38B43577E0dB6047D4737257A", + "0xF5BAE96e458298d5B723aA350E3BeBaf4779CC24", + "0xE7B80338fE1645AF7e6e1D160F538E04241b9288", + "0xcB0BBF3C0A6AF3788C6d1b108EC31937C2779df9", + "0x4c679D206d5C5927e5869B8E5e1502087e185D21", + "0x702c0Ca3D5F5f057671074466311C5D780413932", + "0x8008145DF53e1db0F22737523B029320f6922e41", + "0x9abEebbe3B9eAa8b2156E9989C86aC923bA10175", + "0x51a4047a0F5CBDE25846F540aBA39D5F451ac885", + "0x6608673B607C5F648C347014dF5aB1A492D7281a", + "0xEC4A6f59960Fb55A7Fa49262e2628687b322cf62", + "0x96e4A0657006891D4BA5fB745CF8587ACD1B57F9", + "0xA019B8ad2cC4B2dE16E42cce83aeCf8dA5CA617F", + "0x97af0F60C1894Bf63B440300DDF6b18102f22341", + "0x22138A0CFf84952C018cbcBF5650149017d6b292", + "0x26FeB72790987B6958fCFbF1F53946745F2d3a1D", + "0xBDf9414544ffc5A4A72029AC6511B63CC00F224E", + "0x8b78eb724A22B422891d048012606B5563E62924", + "0x5E4eaE80115A58E3C1fbacaa82107Ef039E3aC6b", + "0x3f52D38Ca3a23c67463f822854D95Ee8481EDeDc", + "0xe66B290105F73dd5f38b25c7d4c86d2C5E5a37be", + "0x4ed7A3c71E9Bf0009317ff24Fbd8BFe96D8D808d", + "0x2445dB49Ce5e58D97a2cb1AcdB8AF326AA592F8a", + "0x493409704eC5e2eD28FDAd4F48638532228aC693", + "0xd8BC0F7888b26e1c71Ff43b487e8C76871D80Eb6", + "0x17D5E950cd12eAaEc810DeE26D9355710Bb22526", + "0xDc5b17F7e80Ba5f2C06E9d04bd202d394165E093", + "0x0b672F8eAdc1fb93cE72bA2966a32533D91c7293", + "0x6AfBaBD050ee883a2E8125d367E5f7F410172E57", + "0x3cdc6F91d41F1738e8E1cbD2A06F64dc6Da5b0c0", + "0x448749280160D2126053330fa7a18f893943C455", + "0xce1B76A218F2d93b564B73958aDeD0e36885c4Ab", + "0xD124524B52053c2710549A359e42d96d1Acf267D", + "0xB2515a7221b2654F9Faae0E4eD1d0E49Aa7B85DD", + "0x933B29E605d3A5461849798DdF0A39100a4629cd", + "0x87664BE9494Ff8B8041E13e338D586e9cF6F1A10", + "0xD80DA4487c1ad77189821dc50696B9A619ee39A5", + "0xeB66A524CBc41999f142B4Dcd49Ce93e8CB050Ae", + "0xdc46Bb979Bf5102d78ece8e9A91DbAec932e756a", + "0x5305095cF75CDD312C202e27F6aAf3CA7da79A5e", + "0x856A39Decda9480D2486A166Df556F78510540Dc", + "0x24617Cf7bABb16681184F9335834a7f053f42612", + "0x538F28B757fc0412A250491e586838E2ea240C54", + "0x7be928201cA49a2337E3951A1372A6Bb26a2D058", + "0xbb49a68c8EA9C2374082B738A7297c28EF3Fda26", + "0x21f701Ae5ADa0d8A702947460488A1Fb43A3Ee60", + "0x0736EB5202248125a869AeA2eC3b15a8F0Fa2Bdc", + "0x92D010aaD4fd7Eed3f8f1B61701aEd63c5d94B9c", + "0xb8aF84C7E9f3731Fc4b8f4308f6BDc5cC5e31307", + "0x8F30c5a59B5284530f57596814b2b2395197E7f4", + "0x39C22b7A4C3aE4Af955D2A6C695d6C40248339Eb", + "0x2B185C762F6Fb49a07116EC06B238B58Fe2BeC53", + "0x66C0d9152209B51977047b9DC3b0B5bf2339b67C", + "0xA3C6312795199EFE026912CDFf4B4e391a7F17fb", + "0x2b05cCa9ce08053B8aA1c3fDC276D41a82c0Ad36", + "0x0B97f99Ee46EAcBFB002761B69B1b766DF0c8FBF", + "0xCe71D1b2443c8a7207965d54dB0f74B29261fA00", + "0xCB6222f4Df04385ea08E8DA2A5871131FF5F6cBA", + "0xb535d4D9126c58d8dA8fE5775088aF77DE37F5D7", + "0x9454f17a6BcC36CFBC8A07011B33DaFCebE4050b", + "0xc2171F490C9eC7D5c6c75cD6d9D43C89Cefc6867", + "0x66cC649D9218BD1Ca46373FEDD6041bCfFB93640", + "0x74Fc147dCAcdf2680c3219c80191121f2Ef2258B", + "0x560D03F2b20e9047714Fea87Cc113D95a3fc7179", + "0x2a724bE1b63caEC1c3Ef95834fdD443c3347Ee1D", + "0x0aEC243F82fFd017666F6ffDF9fA81DAf2546fB4", + "0xcA94F36372aac4B951bA369B3dE5E59cF8CAF1e2", + "0x6CF51FDeF74d02296017A1129086Ee9C3477DC01", + "0x9792E2E8F5a07Be11b0dF35B3bFF61932bbF8456", + "0x3f5E80115268633E19D97D61a2752E1271409e01", + "0x3ea4ea76eFbc4da7207635B3705654BA8D674a62", + "0x126ED947de7b84ab29526D35CEF99C9b72B285A5", + "0x7644Fef96321e78d0158b7B432ab9C58122f319f", + "0x1813c5eB6698250fCD0A4BeBD06b9Ed8EAfF275D", + "0x88d71c6C9a83C56d5742d82536bccC3CD5348046", + "0x4747617cE6A6094bcb67f3c18039Ab233B6e1164", + "0xC6a8d31BDC07860f12e22C615c103400b28176B3", + "0x258302D09995A71B185097738a46Ec2095b9197f", + "0x477bE27B2085D890DF293AA23BCf010363cFB2F2", + "0x71b700181b31747f28a7cdE2c26b503A2335c0ad", + "0xAC631F4F75B5799f9149e70aAC3af88658487f84", + "0xf6FE557e83cE862A63C467E904E3654ACDFd0Fe4", + "0x5a052A9928d100bB68e14Ed2A8091C1a758e1BF0", + "0xEeF0b8C8388a332973dFa7A110eEF1f0de8670f0", + "0x37aC09e1640577e1D71E3787297A56b58f88F0F2", + "0x708dF04E03AC02c440937Be6e631FA8B46CB89bC", + "0xd4a214689898eAe23F4A36600Baf0FfF1889FCd1", + "0x8EBD118444D8239505D66c8Cb6Ee36eee6aBED5e", + "0x6a087212B8B9fed39E0874Bd37a5236236EE8aCD", + "0xdBfC3230Dca0b50Df050ac5D3D42BF1eCbA1b82D", + "0xACCb019890BF879d19f5aAB1db068057e4CEA0F4", + "0x9D0a96a24d9c4033C882a3c4A3c33Ced4254581b", + "0x76655FBE50A89333B4f175C87298a6E4BE4D2797", + "0x383697a7feF14483920BA9be57C26115383D1DdA", + "0x21bC5e5558e85E1e49589B4B4fAb796ee446fED1", + "0x32A1874657673C8403059cb30880612aF7D2C9C6", + "0x04ce112FC3832fC423DFAdd4e344DeF6A4840fc7", + "0xb7360b4425BAdd4bf91725796A7Ed431D8bE8E43", + "0x10F73c1eadCF834a33061c4c8a962E528D6ad883", + "0x2F5a662614b84f37F71F25E4F56b7562a0e71786", + "0x8eB9da111684294F6d57C8C400c71D33e1221800", + "0xA82d4D63fc6332ADfb593CF21A649947f8D5464b", + "0x7822159ee394D14745Cde63a706F965fB73c7Ac8", + "0xE2D7C679AeDc71DCFD65Eb381107f8beb0F65666", + "0x1C00268fa3499658124b8FE6789D371c0a22a21F", + "0x82AB77185a80A5B74C92d679a02b28B115F21962", + "0x02aa0B826c7BA6386DdBE04C0a8715A1c0A16B24", + "0x7B230F8235f3E3b14AeFd3ed657479e8FFD931A7", + "0x1e57D64A5ABC4769aFe6c6c01f58f666e219F167", + "0xED34101ec8916fe669407777023e71a70C462031", + "0x9D497F015fC1f853c82C016AF981336ca3aCE04C", + "0xfB4dd74260f74FfDEB141A1a8f209710c5ec9d07", + "0x6ED88188Da646B6fE64022895dE4045C35EbA48e", + "0x5538793f0eD24fD4EA2Fcd5225A40995A8f5Ac3a", + "0x3187a54a100e7DfA4fA60c5977d1921223619F2A", + "0xaE9A5F08DcdD84aa4579e0870A07b62aFf19E4e8", + "0xBaf88B32025AEAEFEE010013dA46098382475DdC", + "0x8bd7AF715cdb9f5d2AbD5dA5138671eEa19EAc70", + "0xcE29cEc7552ab3c9Cf5264Ed96081c2804C3ef07", + "0x6458559c5489f3B9274de69294B8C1AbDDa867Cc", + "0x7D1B10577DA49D490685f8105bEE3fA7A82ceD53", + "0x52B481e0e149a882dd26167e0510bDeD8C595330", + "0x109CbDCf9e4F6Cb9EA56c810bc26F06f8b1F399F", + "0x6A08CA93567d477C6699C50d3De6eaeB0307C8c0", + "0xA3e17CA80549C16CC850D37acee1a02C45736f68", + "0x0495C779401b2D9f8C56d9d156f4B4b653Ffa2c1", + "0x0fD90eb42aD3726B8E5Dd0604D175f2aA9F10aA5", + "0x35B74170281bee84d414202EC5f16e4B1aF7Ca39", + "0x004ab0F6171FF2d5cb44b6d50cf8Eb2edc56105d", + "0xE4D97EA3FC1459a8B2643A7cb07f7A4471F32F8f", + "0x4D4F26bFF1022EACDc8369AA12BF800211BE86aC", + "0x3A39899DC78b7307aCd83f51463C853Fa79E1B09", + "0xb798DeF92b18861602f956D06fa5CB0bee47eBb0", + "0x7A4E47a4E99e7d5Eb9BB5A619E4475D1dc7B3ab9", + "0x18593Cea6fEC4C6351c42f99f4daeD0B2751Ce41", + "0x79A20D3b60e7F397D6C870C21335decB105Fb1C0", + "0x31345e8a872C5a34e6264b1c2aD3fc8D13a0734e", + "0x5192Fa6618869fFd9Fe6230D94B0e4C8F57ADCB2", + "0x1c93bDA3BEF1E486ff31C745278FC3Dd0aADD6cc", + "0x6169143e0f2452bC5c9642EE64ac61a1b6c9211A", + "0xa12EeC92d8C155C8ea9159Cb87da4e062BF9992d", + "0x0FA88472c52491DE94d0d40C4EcBf69B63c6E300", + "0x3352cb989e5cf23335AF7f556E15b824f7e2463E", + "0x175079d96B783C9e9CE35B33C2968b683531ea6B", + "0x06Da2826dfa8d4473DD2a87Dd28fD186c81b0073", + "0x253e11bd6b8e8f302D29622F37D6365a27087306", + "0xe139040AEb075c3F0bED81bb66F567b5AF19caE1", + "0x99525b18Bcd5F429e25F9c00863A917909cEF3D4", + "0x8D7E07b1A346ac29e922ac01Fa34cb2029f536B9", + "0x7385f8b5ab1303C8E476d371973DB768F1a43Bb4", + "0x52a950D98f11fD9eDce6692fEe7cc8e110A471dc", + "0xC2945Ab7579b4c05Def2cEA0f404268fc75952a2", + "0x584BBb33b6Ce22DBB8BA8c3CB02F90a88F88451c", + "0xf4A2f32BefE6eBE65bc2696969cD1C1A34E0f92C", + "0x98fa18f31Bbfb643F47Fc866262Aac55c9824917", + "0xEdf6bb53c523Ce45e94C5447afD7a042678fefb1", + "0x11EcFE1260cFe3c4ae27e579dC014Ff83482D9Cd", + "0xC78E8E153D926047A07ED9A8eCdf837061F11B86", + "0x8016aD7A57Fd087679c57E766D0cC896BF43615D", + "0x8ea12BC08b5cB9e0d9A38C90D98b35AfD966C3c5", + "0xf102b03611E104CaDeEb7605Ff1d9FA5c07Ba115", + "0xFe3C6993920Ac33bA28378A9f92e18De52795117", + "0xA40f36d4502a05d215cdcbAb5693D10e271A587B", + "0x4BBA887158645192DAC8aBea73D6aaEBab2B3b13", + "0xEA8d665651D7B3d429fE6bbF6F895456eE2B6B6D", + "0x5aC2Bf9D420505E61a601143bb879AD02F3A3746", + "0x2450d8083B5eC7C2E9c1122085Aa23Fa3539c64E", + "0x7CAA741d741a38Be582fa619eA51f4657d85E85f", + "0xabf124216ebc84F1DA154e5e920A7ce6269535a1", + "0x009994eAF678D11b17151455Cae0C87E0b2B6825", + "0x1867608e55A862e96e468B51dc6983BCA8688f3D", + "0x4a4AbC2439620b311e6E044Aa22841126ceb9CF3", + "0xfB5c2BD48E34Fc530eb857aCd71905e2b17b1F9D", + "0xEB057C509CF30cc45b0f52c8e507Ac3Cf8E78777", + "0x8Fa4Bee3787714f25Ea4E6Aa16b27438FDC0a5dd", + "0xFa4a7ef5D09F454114FF78e7A8A23Eae71bDa600", + "0x3B647c93E1B60F41ea687f1C9EBc6DE3Fe52f475", + "0x961aE23F0cD4CD82FC129563Ae23D81223Aaa2D3", + "0x2F8a99D208b3d09d2A944e27d4451D21EAcEDc21", + "0x5d77392dD7ac01aF37639eA303FA44e980C83f79", + "0xb6B2A09afD721AD5742566FE3f02aCa3a2a6017D", + "0xdDf169Bf228e6D6e701180E2e6f290739663a784", + "0x2Ca99Dbca3B8922b6b4bE71554e543254f11B0C1", + "0xe51D044586E4f3377C099aA67Bc523b3B3cD5119", + "0x39bbE14F5fBb3276d06760C95a071ebaB6529192", + "0x1d0b6015C2aDE8ff59DA0e2e23d0655fd8B3B3B6", + "0x987c63895621FdD6a1e7A6414043b561982A2354", + "0x0f2f90aA248c8960dB7a5e13a52f3769bc4aCcCa", + "0xcC915b7884209e7C6faB158F57F854517f4A7bd8", + "0x3acBC3Eb173FB19A3681c6539a1e108Bb4cFaD8B", + "0x24F5eE0991f95328Bf74b682F6faD54CFF9C832A", + "0x19c4E72F327ccc3B47e6749dcC3920DC232C8DdF", + "0x8CAeF18A4e4c68A1132AE195E628ef68043CCb05", + "0xEB32515Ed999140479893d96b5803e1c0B014bcB", + "0x2dcc372E20e0c9920b4e2d7c51C80EB92271c209", + "0xDfB79Cf0dD1890f13EF73f5e248b9D61172173C8", + "0xD8e51fc9A41C19bcA7646dC054F03c40BC5ec6a8", + "0x906b4a9f69C1bbC14EdC8D141B4A22DF6726592B", + "0xd3d4f998B66F0dbAfE5645D14f001e6852271Daa", + "0xd4fe43Adb09b3753A846b6818A0A50Ea8E4fF188", + "0xd8b2B7F42873F111348c835563e26865474337db", + "0x6d0163c1Cb3F4CE3F7528E2954D59d77Cc8A8053", + "0x1E6bd36167620641cc88C846B815adA76F754e5b", + "0x7267db34b6072080923F92E626C2Fc5DA91fF25b", + "0xd04f2Ae9B9C0A5a80812e901733d799D66C78b9c", + "0x05e5ab383f3C2D423cFd118B83420261ab0990Bc", + "0x181a15c71A37b4b829477Dfb7cf006F0493B6f86", + "0xF8253EAe286d2B3AAab470b8118E60b86021Cb2F", + "0x69ce31018C82CA3D2d9E4C5a6D83161B4320f9e9", + "0x56b32744bb8AAE79f34E7701E581cB6E2136E0D3", + "0x4B240321bD08Aac0b0c2ec441d5bB1f7190Ca0a8", + "0xCbD6b0DeE49EeA88a3343Ff4E5a2423586B4C1D6", + "0x3E5944908451231f22092218129f4b7D525E2E61", + "0x1A5CB0C69C2e607CCb6Dd7FE7BCf394636A4215F", + "0x0dD16908663523E03C11C0dC14FA4276C1010a6b", + "0x10890742A1a20A936132072C20Ae77b081486190", + "0x25E668E561f617845F044267AD0dBbdc9aA36c6C", + "0x43D4CF2Fbb008FCe9548d94a35C3F318a4940055", + "0x8A30Fe8DD7C1B8eE6fff8892731eD345b9E33B29", + "0xa3D15d2EFdD2A93e5A4BF07D69efE4A142A347f6", + "0xbBc711EB29527E2b86049d41B867Cf009d0F1453", + "0xC9B7411fd802BDfA055cFD469D1aeCAc2Ac64394", + "0x825736E7336232CC595C345D049474226Fdd154e", + "0xbEB439195367d87184733bAdb1f4F26A7df9C576", + "0xA016B59Cea666f53da4DA3C9F99B97dA1e0b016b", + "0x45B6209b736912E6C4d12D3650b582D581B28978", + "0x7911Fa8d5bfCe62ef68B714D5b51A65A5732B101", + "0xE1666213AaD05DedfF07886813fA3b88F7Efca21", + "0xf64a67a81A0f9bdB67fA232918859Eb6c640DD4D", + "0xCc83DF78030Cbc991df847f353b189fDEC97D359", + "0x78fe849a5e90A7f5EaDe2b4dc8B443F883946d5B", + "0x083df0282Ab7680098af6059F0422Ef87900D22D", + "0x6403c6a27171349324307919039c452910eb3452", + "0x0c9C3Ba64072eb566b0E9A4B6Bb0D7B204d68469", + "0xf38a9B66F90a34AE03e21d75F398E60Aa895e45B", + "0x488A6A10ff29B278f03575d466A1F9D6bF494ba3", + "0xE399ed9a36DCd2f39cE0398eAccA536C4F2d9d1D", + "0x19799F272574079A8DD591C13F7aeD0d2f7AA011", + "0xfb15339d187A76AF62698F73D700E1F8536f03BD", + "0x30A26c2837e9Ad41Ea5955949F00402DbF86f124", + "0x8Da3280EbA3BFde4Bf096b4E7b3Ea45ef70Aa3A2", + "0x7543dd4EE62bb86a04Bc133d0eDD3f19fb711324", + "0xC345C4d05C0ecB55A2946Ba0D2bc2e737Bc9a2ec", + "0x5Ec850d24F1a5fb122Cf9b3446f431D1f44a4119", + "0x0d268e9b0DC5870fA1188bA99feD52eDcc81A985", + "0x3214e141bdAe08Be1f382A885cf3d2a448A5E780", + "0x83fE7223CC6Dc1430DfeD5435D4a264F2b799100", + "0x7c1A2171553460219fac450d93ED2051f61165B8", + "0x97F0d7f9d9e7Fe4BFBAbc04BE336dc058873A0E8", + "0xc283E7977583B2d17353Bd17d01d953A770A5776", + "0xaBBf75A59AC8838FA46bd5260501B68ab28B95f6", + "0xFa537C114EDd1D85a6019222C1c0e90707d2088d", + "0x5AAaceDF5a180e084d5eBAc2E55Ea484cf0d2780", + "0x060e02320fc82a7Bf148a443C34920204d56E7bd", + "0xFd1D918196C4586a7D58cb75f1aCAe330Ff5f348", + "0x7a44834D4dEC3F21a341dE51365fDefdD41faA98", + "0x9979375A29d542A16d1D7042b143C9Eb07CEf0EF", + "0x67c4064fd8cD5693458d5efDcbF93DBF4036384F", + "0x0CA1Ed3c27687a46DC287DC6ebB4974AE5A0c3d9", + "0xD01C4e5e9681f348B9CE2135ccb2e871081cB239", + "0xDdEbbf1bb417E4a2Ce7E1E0Ec82ABD77A899DE51", + "0xe78705fA043687cbD7b907d91730d1FE5CE66365", + "0x88e64d1eb51d4dc955568900097F01D6af953a3e", + "0xb9999D5f3AACc5c92e1eC4FBD31139a91b24FB08", + "0x99C04ae385E3c247D8AF67b193357B79ade10cf2", + "0xa9eAEF87c01B4C46a691862c7Ba94401394B8B9c", + "0x2408E836eBfcF135731Df4Cf357C10a7b65193bF", + "0x0055D4369a59bc819f58a76ECC3709407204dbAb", + "0x93adaa5757cb116cbE445f41E65108425D5b102E", + "0xf38b07B8ac72Ad70806E902c2ecFb7EdD36cA3f5", + "0x89D1a663b53D5D7831f67719f2fe4932f8251F9C", + "0x93a4a366dE322dCFC68b629D6086C6b19Be4aECe", + "0x84669952f2b6309B7876265e7037795f5958B388", + "0x66697dabFfe51a73E506f463b27E388512ed6Ecd", + "0xe116d5F4CcD817e93a4827B4B1166fB3fA61BEC5", + "0x9F9692FfF5Ce2C81737f62bccA101a7a7bC31c46", + "0x4cAc5d5403996Df53c3b2ff5058415665dB2Ff18", + "0xE434109eBbF886FbFc2a364dd7b78BD3d79279Bc", + "0x5952bAb55191f8A815Ccd8BA4945F7a09Ef5DeF7", + "0xf90a71D46DB26a7A3EDcEe7Ee2C93a10E79EB5CD", + "0x19879714826aaB01A7cc90eB7C254177C460A36A", + "0x83d880Ec71546746D24e7fa28E1eA3964E236d82", + "0xd0dC07B98769f23A7BDbef15A35Faa256CB65dCF", + "0x0FE1AA632A2837239f218e3Efad21E8018A9F4eF", + "0x6720Ddf5cd112a57Ea30f1f16B70f6F213AF71e8", + "0x9be8D78f7733681189AdcAa5634a2dcB53fE29Ae", + "0xE88a832323ED7B3D29Def7Eb357AC308Df3673c4", + "0x93617280d57564241b4aD4F4e09e02968F3F2D76", + "0x1D5156BeDd77567E2246ef3B77451b145C77A347", + "0x27936eCeeFd9F1e30c20F13F7BC91cCdcFC2D907", + "0x84Fa205AbFC260106765E5DaAbb1346Db354Ea74", + "0x190ac830b737AcfbfA819F1D3aB0a6D702D72147", + "0x8a1E24491a2B76E94F69bd9F980D0Fbb8c3B75bf", + "0x9Ebc0B1Bccb2C2412B661510F2b2e67828347F0C", + "0xAaC12ef7082D58bFff1E0796980370A5BB544196", + "0x0DD4707643294200BBe1115D7E414E55F99eA61d", + "0x6c7286c5AB525ccD92c134c0dCDfDdfcA018B048", + "0xA60AAdf62907bF06Dd714781d44Bbd4C2d783259", + "0x6ba58CD30014A861b11eD429200Bd1DD8277DCf7", + "0x7287c3d93b89B7f9153fEE6Ef086Cd2858e9B9EB", + "0xF1D508C2cCb2De16B20c2e07869408524E1a8D45", + "0x2c2e209465D5312e6dF0cd5F7D1066f1aff9a953", + "0xf626e9A2FDDbf55b0b1A87C56128A7ba6723A85a", + "0xD08B4F65Cd97123348F41e684f11e65bc510157D", + "0x0fF6E1e68413D2407Fa6161ac4Db8f21f4D3aD41", + "0x28aEF7e702afeDA80F05Cb8457d49cCebe7F66f2", + "0x064C3BF2B2A26062aC038b95aA2b223CC8968040", + "0x818854b363b90791a9eBc29e2f9c7f1055ee5A4D", + "0x77c4a515d6779f358d9C70b78c18715EE352CadC", + "0x170a426c949caF6248Df60a9B412eB3Df9d32484", + "0xC2c0331897B98c72b092625c473867359Ec5f6cC", + "0x3381Da659Df786762a59B658c43c850188808205", + "0x9C1dCb699e6aF3AE59b7219e5EA7fEf6D9FcF9C3", + "0xa13910B72ce4C31F08c84842ff0a707f1c4Db389", + "0xf6831d052c9dc1523c33E7BB9B657a33AB84D5d2", + "0x9aC63a553219198ADa836731B2aCC1596987B417", + "0x62371B45D283E13Ec4C6666D3f8f0Aea06dB78E2", + "0x5d81d1b2775794E5BCE61Aa75FcF8778D7d1ac89", + "0xFe6E29c244DC1D06C26033fFd056e0f81224b64A", + "0xa38F13b62142230F01842f63A014AA59652EC1a1", + "0x2C8cDdB4Ed5Aa7d28fcfCc75313dE4286e0b5e32", + "0x53eB9A6f34e1FeAD9Cf1013c7058679bcA6Acc43", + "0x39B2bcbb9fA29D72755AC2446C310FE98C065A12", + "0xD80267de5eD5de6F7D6C66986b17b138fe2e1220", + "0x9E96A27867464C9f8A48935b143Cae922DA949d6", + "0x00E8ecfd62A04cCbF2b22E7fBb6992900151ff77", + "0x9d0Aa99Cb5E64D4Fc6203906c02eD2409c9E78B5", + "0xDBF4a4874E5BF633D8628e5272F9965EC1Ee4dD9", + "0x79F38d7d21e8A3d2E91aBc2Cc2eA6747DD5153f9", + "0x7f965552C63da3Ee12eDDF79aA74B3eA6bE16D2d", + "0xA770C94a39D17075ca9Ba359b9ee191127649Cde", + "0x3b46A777dd4657D91eCc9ee3eFea1162AD1cD9F1", + "0x97992871D78116A928aDD496850ec410a31Fd564", + "0xd12630b70D3c4Bc35D9a9B20C1Ec7493534B400e", + "0xb334C11D1612231B60aEE4048fC6B57C3ac91F3c", + "0x4F00a16d909E3C9DF5b5e7be30b2aF0b0E67C5a3", + "0xF28570694A6c9Cd0494955966Ae75Af61abf5a07", + "0x765fD440bAcBd1A34bA08F0ffb823fce62F06c2B", + "0x0BdA0F33311E65379461D0A555BbD7669c0eaA22", + "0xdc0D491BD92861746C745b257edfd8Cfa631470a", + "0x88BC11AC7eCc805b024ffe559F44CbE9957c21FD", + "0x7D3A0eAAFE6CBf41bCf9C2f091065eAc9098e0EF", + "0x37DD83EbDE2d144bF68be9a6686A3c9BDa6Ff73d", + "0x57C6bDebe4702beb4Ab070c903EdFBEA575E0923", + "0xc1A0bC536251215FAfC9ccB610752D8453545138", + "0x6542FD1b7d7584f446b13564b0fe5214C8b85bF7", + "0x78eE7f30a9e1A2B1BE52E924c9fc7b608177D772", + "0xc9C9077be44B06fF3Ce957D72149C65128F14331", + "0xE8996228b2A7A7937AF6eCB2bee5b99bAba6aF3c", + "0x94bb64f974b5Cc1aEEaFaf66aC43c6195d6e7BB2", + "0xf51e5a0A85d29AA0508894405D734BcF044dcb5b", + "0x19Db2dea42083d3b30Eb89B1c30ac5A7b91de42B", + "0xd0a2ab6A3bF6904093EC8FC5a8c151E3c4543251", + "0x747276019e3340104C96397bF6537aD01F93D7df", + "0x51256EF00c2485F3A40fE1b6E4E779147BCDbb58", + "0xa24b04B313F748cfbA45E2C78FD507eb85dEB17E", + "0x6b9E4F287d44cEa39Cd8CcEDAA42488617AA21A7", + "0x67bA1C914e6b3b396346a6498C1eF8e59802B1e9", + "0x885AA2a20e2e854A5cbd8aDa548B7f4A8FFaA0C0", + "0xf903672BA62039591812141eecB4CFd9416CfC83", + "0x4538Da8BC4108a1f1c20eA04fA1ED33Bd141d3b1", + "0xDD043889435603F2B99A64B99A065711aF3C02C9", + "0x1E9264e33Fa02406E41De611962a72653EE24Bd6", + "0x161e82a787324AcfacD0D611faF9aD13DfaCd65D", + "0x8c01b3606b5466389cdbA890Ea981C23931042b0", + "0x214138492F53fdE449bFb4e91B91e3e1bA342264", + "0x9084249d04B91988713c24c0EB10D25b90B4325b", + "0x920ECAaDE776B466A550EE3FA6dc860f1A78f78A", + "0x69258d1ed30A0e5971992921cb5787b9c7a2909D", + "0x709Cb5D672CaBab9D50250C0Af982c52B1b233a1", + "0xDF2dBAb86F0cD5eF39Af65cE8411f747F36a12bb", + "0xD7E17834f4aEf24F732DBb6f0D364ad5FDE9d516", + "0x4FfD0A59a26cB2Aa76D403215e4cC2845C053994", + "0xeC898Ca8A76FB722A1Db369E69CcdCf02f31f4F4", + "0x393A256dDCB830a837fe821558E342D096a5f54d", + "0x09dB9b873Db84AdA9059630f79cd832b4d576800", + "0xeac4d6F1858e6AD8E99C37FC390A4cbB4d519d62", + "0x559C027192F28c0aa3f9F531324C7dADFaCEad82", + "0x73E8D574264ACc681E8DcEbA54f36F5eAEDa7005", + "0xA07BE4744Faec9C69a92D654F5d2332fa73Bb87c", + "0xE57d3adCd6818b8e40BFcF4EAd289AfaF4D40b5B", + "0x5BE23238eBC1A79914f4376781c71a2B37c49892", + "0x42Fe01565e60D0687Ab793dE8caFc1e8a39816A8", + "0xae3D9aBA194740091EdBEfc8619A90AbacdE8fC3", + "0xFaC23A1391b42a1BC4A17F972be2555236003870", + "0xa049AFeF83d112F9B9Ac4E9d743C50aD08EBEe01", + "0x7E028136AaF176B8E338581C0e62857d8b7f5ef4", + "0x47C869583F24e7c187F4d57CF1c2736dA51F145b", + "0x845F8EAA68D4F75562325089115B470080F04052", + "0x5d02c857E98465f5b3a957B1f43569C4dAe58cA0", + "0xA2c12dd46eB80ad3EbFB9F07B0293b2e076BFc5B", + "0x88EC8e63dB2f705e518e628Bcb6cBBb0A4f170b8", + "0x0A83985E4A6E8Dae2B67beD4f2d9268f6806Ce00", + "0xef252a2c56d61D5417BD59E2586aC0aDF3ce538e", + "0xbbAE0E0795294Bb37F9B7CEE715E7F15602f67DD", + "0x40Bb0bE4171a888dA2a6C46aC6aE505A63FB3aE9", + "0x8E77B367fE90d91b3f56Be198Ded811b74c82BE3", + "0xC3A03c3F58C674FC41756744bF2210C8bdbc4081", + "0x7A0e7ADb1B9A40a4B4656ad55102d46DFdD6C19d", + "0x5d8d5184B35F0569B79d7E169be0aeee21A3C078", + "0xDB0132c875eA7A00c4a6283da592ae6500205396", + "0x123b121BB9771cB171D34c835E92F891759Bd79f", + "0x0d2Bab6899C0a058b0f3162F656660A2F41a7712", + "0x8e6ed79c48944265FE5D004010c11CB28aa105B4", + "0xA6cd97Fb84c669B636B86e0C2CF5e3428363a6F6", + "0x24E1C10d4765EE61840AEe6Ca38088023ED67435", + "0x904BeccBDDE4436696f14d846008818495AA616c", + "0x37df7f9254A44ae8aCEf12f9133724bB78F5E5af", + "0x0B997e226B63FdED87673Bc8B43bA24477b8147A", + "0x3722d1a53fBA65dC3C2AbAd6C5Df0f1C969699d2", + "0x333287aC13B0Bb9251464DafFe18dC309aaE770d", + "0x9aAE12dd452f78ae8919C2168717e7348392aD11", + "0x0FCfe3C0d30b88B9b06F85DB087439c524369E51", + "0xa3b926f6d2bB5507fE711847640bA2086CB11A75", + "0x12f787d5833b8B65F9637A5d4bbb163b7db531e1", + "0x1591fFE832548d7ef6152944CaaEf37fe313381C", + "0x229A933de3d977048E07aFC630590a91Ab88c47b", + "0x5d47e5D242a8F66a6286b0a2353868875F5d6068", + "0x8fABCf470152e44E01750d50f3631F538b8C5d8a", + "0xA1CA3A8f81b56407ccF4b8604cD2bec4E005d909", + "0x7495f82F53F151DecF4Fe96eabae4f395AE3e2C8", + "0x63C913ae5E66825b21D83AAc5B58e859Ef2abBcB", + "0xdED2B63AFDC317E226D6a43780DE61d013C0acF8", + "0x3b2cBBE7fbC39846Cd15333B0e2F3FB89eEADE2b", + "0x46C4c4781fBCbD501e11eF8D87180c44026a3EC5", + "0x528935B9889780e8FFe3B3AbFb614d31718D9965", + "0x2c803dcA5549862f778cc821eF98c5b907b67cAf", + "0x8581dEf657860bF58A5b0d6d2D357B51B81655B6", + "0x3714E1078Cb6F67eF01442B4213012B16889926F", + "0xcA7c767854B1E7305dE07247AB85E30543d1d9c9", + "0x3b6413f38da132c1eE3E1BFe603925677e5CeAB8", + "0x85312D6a50928F3ffC7a192444601E6E04A428a2", + "0x99F06631425F01514e774Bf3208909fdeb5305b3", + "0x8c11E3Af9c1D8718C40c51D4Ff0958AFcF77fD71", + "0x8397bDf754Bc77E08eDAea82E88bDB6ee517a82d", + "0x6ffD435b88902388D5A9C65f2bCe6a75e1b14093", + "0x6386AaE7B2De8e9Fd895171f24F93D08545Fb4F6", + "0x32a534bB6afb78FCCE6Ba6aeB394B148E1C398e4", + "0x32477E70b660272f514978185cF14a0c6Fc68019", + "0xA9abe4e7cc5aAFf81Bc02F39D5f2b56a8094C9C3", + "0x62504bD6b512aaed4F6BE3D61B017eafDfd3964f", + "0xaC47e58de553Bb5eE2F30Fdb5fF8E6EdEBeba90d", + "0x99a5AAE28Eac0Bba6e1e2f6c1C0E5a87612DF6F3", + "0x648cf383a4fE746a0e507fBB522e6eCBFFc8EC25", + "0xfB563Cb2572c50480f0ff1A009DA54f1fC5D9AF3", + "0x28B791300024FBCE569aB163588CEFc7A5cdF546", + "0xA7195C71AA20481fA0c093E51323F8A4D3a2F5D2", + "0x5B3067C5e26fEC823d6fBf8F315e43632658D2F6", + "0x3491360D98559EC4146Fec5b9886d7f9f3762503", + "0x1A72992E9058DC444f0df356eB007F6aEbC6d867", + "0xa756B300E0B546dbaD3936d4B8fC9BFBdFe222e7", + "0x43C4fF14DAe2Fbb389Dd94498C3D610A0c69a89d", + "0x09cFB07583c610FEcC6780254D354cC286F496E6", + "0x73422A2722066C1750cb9fCbF79caBf1f08f9bF4", + "0xa0F6f7392283163E133BdafE35F8CD7d52520ED1", + "0x73459Ef0A33EA718211e91504E9CaeBEc0e6e667", + "0xF786b0717f045595c5A3904262B8EF649Bd666F2", + "0x5be138179A1516e24CB13EC34112a9C4dAB7b384", + "0x41a6ac7f4e4DBfFEB934f95F1Db58B68C76Dc4dF", + "0xb7b570Dc117b4946925bc39166418516FB011CbB", + "0xD1F9465e0AE9BAA60D4b015EA21D6C957F118896", + "0x5bcBABa9704F625bdb7EB1D8a3c305737916e6Ff", + "0x2FEf6742D30C81c518d7742D5c7Ae6723f64a79C", + "0xbba2379F5cc9A2f248C5Cf18aD72379AE2478F42", + "0xe379AD19528A01dE884A7CA6a7fE480Dd157c182", + "0x0a50F5F6301D747E2C67e26AaEDc38B9469a8db7", + "0xda4C20Bb77eEa27a47d27adaBF2071328d93a67a", + "0xcdc0b6ea0875E8d7519EdF7942625111685D6e9b", + "0x01C81C0E0cfd968A96CF08E3dC32f395A9dC075f", + "0x81db2dFD33b24419F0F715e331f678411Eb1ec30", + "0x7576ABd7d9465591B6A41d7d2B77E0c860A99878", + "0x54b55662901aF57B31fb6B52AF8175b652A5816e", + "0x08706C91ee7bf8016f8B33F3530FAc5478120fbD", + "0x7af2fe0509823Ae1Ed416adBf1f80D452116fcBd", + "0x62bC9BA8770F43011d3339ef30F50C30b404EDcb", + "0xc067e7F26f6e75BB6591Acf1FC50b6A5DcCf5835", + "0x6828459E3ACFbF6b787C4Ca752D242e6340D3018", + "0x2f843eC4bA0a0C14813EC00fc20F7BD87CC08F63", + "0xfe701B7793Fba9EB4E74F1AF5863D84f9d442091", + "0x4248C2aB690289C5AdceDF61981796bEaF9c3fd0", + "0x8fcf51B7562f2204518CBB8D678786Ce09b4106b", + "0x84807BF486A2Ec97Aa667c6dbC68f73D587bA150", + "0x933258BdDD49beeCa77F6D1889633c5429AF45EA", + "0x69e039d6010Bddf601271c2B51E126f987850c68", + "0x64d1D62cE19332B7D247b82116B50d6896A0A64f", + "0x6e28B3331C51A0DDb221ff833D499a2bC439D002", + "0x03F248f96c6ad8dD0087ff28f0FaB1e3205CB3cD", + "0x6396a2ACe5978c4fc9BA7AC3D2d3efb6B3055461", + "0x3720E6888aF0C9f5e3b3197F133924232adE531B", + "0xFA122D9eE820873027411FFbDa06853439B8Fef9", + "0xacf6adE9613A4d9b2801A8a490C4BBBA5B9f1ca9", + "0x9188714419B81c4c920F04876C19FfaE8f5FCC26", + "0x767504c377Dcd3985a55760E7c20599bfb21291D", + "0x7B36B298035e9925f46E7d3AD1cA16fe2bBC46C8", + "0xF6F8Bb3bf0b250D4B41dE6BC20FE57DF62c045E3", + "0x8Bf48230D4c10f011C4d1B73804E20436c239902", + "0x1a9836F6Dcc253332eE707Bd9808aB3d79d973D5", + "0xd2E538812961E563b82a13662a8C22841C1E459e", + "0x1124281Ab6bFBd169DCCACF43050527D5db09C6E", + "0x1F6e332E2a1bB6DE6104Ad18408239762770CCdf", + "0xACc43B5c8E2ED12D374e74cD91bC576fBcB9eE23", + "0xE9a4aB491942160Cc1F42449911D0E3dd5222436", + "0xAA56C1febDB4C9698CC09B6065eFE23443474114", + "0x9C1eE1dddBef7be7C96dF3BE5c63998f18f4C9cb", + "0x3fe7F445566A103Eb5D62C7890387F717fC06e83", + "0xdA294570098d1fe503BEAC1ebb63980bfC48f4d2", + "0x5430FFC48ff3A0D4E53574Bf89a7E9f75eBA524d", + "0x092121fd7700B97e68248f8763c4F3cdD955d5A0", + "0xb2754b61d93C0C0f00b0BE4fa7D2012B6Cd2B430", + "0x1874281e2386d85617eAb117259f2157334243db", + "0x1ECdd6dE99277d4228A9De50Ea73ba7e10D662e6", + "0x4815Ee939fE2efeC2f7bc415f0cE2282f6417fe9", + "0x83B285E802D76055169B1C5e3bF21702B85b89Cb", + "0xeA09CFC7239476AB030B649D5CEFdB786f0e7412", + "0xa8b4C7F8B3d91b324F815252da74884E68FB4C4C", + "0xf99C8873B972b4192e789F55AB954188D0d9A133", + "0x9b86449941d2f226A2211553f722C76c06d99bbb", + "0x1289c5314B7524491418FA5200EF2F966a3A923B", + "0x62b2b7D015d314e17A4b61553ddc6d205c0aE2c5", + "0x4bf4421cE8208b2e54C0b7e0B142192b47e69C4a", + "0xdBC35CCE779E29fB71799137f632b3f6F7f46573", + "0x6620B86Ef016e3B9F57A9dbbC5F114311D4F8D28", + "0x3cA99a967825891e2Ae4F5F05367AD7fA83f22a9", + "0x380fEFf90198bC240E1c47d4e3C0b51391DD2D63", + "0xEC57915526A7869b9b5539B31c90bc81a060A171", + "0xAB9b83a7F3416d921464aB018AE273fb4b284E86", + "0x3BA827c00bBCA8d1A6a81b0A590CE9C82C0AE317", + "0x1f05DcdB58A47a33d7bcE998FED28e2618B4f067", + "0x445726b59A24628295CF52e8DdFD3617b94e3932", + "0xA294a7623F538ef9C0497Fa8366a1D27ee46C3a7", + "0xA9D5E82719626496292be0575badB566551573D7", + "0x2b163Ade152Bff560206dAf5C89aE358e5044199", + "0x0EA41755EF53F8ECf448b0c19e05255F39E297eF", + "0x6803BC6e4Bd209537eB356FeB03c66bA4A383f1C", + "0x1EE840962f7E8414092A25a216ec94d06E4600d5", + "0x2e171682d2e7962fD7518DC2603F413cdF225F5D", + "0x00Dc3a8455daec001E26a5Cee271689D11bD98bB", + "0x318c012875477b1FA77bF1646df59B90272A0E26", + "0x44E8A67E451caFa616AE785940E9C4Cd574aE3b4", + "0xa5d6dA2B74f2615710436fA5c08007A37d97d701", + "0xd27bfA0BB69bd04cB869660b2EF97ACf0Ee3A707", + "0x0Ff324D42ACdCF93Ed7187CB9705B4609B074BB4", + "0x60305F33437a3956eDa0103d4e101021CfAcA6E8", + "0x01D82047C4A758b7D5c94F45fFAd6FE48F5D8EC0", + "0xf93d4eC4cADeC63A68FE13aD5BB1Abed44ebb81c", + "0x9388332Eb6465DA47363322De1964970A6a338eA", + "0x7511B3D9c01Da239e4f0c3D48E190949f68E2b03", + "0xa1091B710fB05BFaB226972aD70EE9ff0AA0dBf6", + "0xEDeEd75328937a17749C1a59772E2EC79eB51314", + "0xd6Fd705EE0B31a9300D3E2154bccE777270CBb6f", + "0xE65Ee5600293aEe2B70a60A48CD44A492eBb6974", + "0x93D87D3488B2774DBa828A53aF99b083587BcedF", + "0xDF42507b9656faeA7Deb14b8d409F84ed3542DfF", + "0x3B82B31bBE0DC3fE0f13D17f9a4d0455eF562e06", + "0xD83c4ed9aD3aeEad49b47AF8243d6DF6A56B9028", + "0x1d28255230A90F4C4Df8aB91fb9230C7aFEBee67", + "0xE39516C6eCB2C94E181Fb5b6822D70278fc76545", + "0x61C5CDcde2150A3b6c9CB27D14fc0AF0f42Af5C6", + "0xe0e4Bff7ED1Cd39Dfb4310eebf078187586526C8", + "0x3ED894DEdeEa378C0B8e9fE2DBb29e0525496c34", + "0xbb4d109c3dFe45fF30703BD2dE43dBa1c74cAEe6", + "0x4B1F140a82cF40A955D1e8cA2aA52E128b593722", + "0x85D5f9AA64848677b5972603a04a0fFb2F321038", + "0xF1150F15E5Dc578Ee6a960569bE240DB7a9efdcA", + "0x2b131FF66b1CA0348E1c20cdB1E5069485405f59", + "0x149396B7f07EECdc4024Fe2B58EeCDC07C356De1", + "0x2aD8aE2e1421B1d0a0823C0e8f71D40cEaC4F872", + "0xD13Cf36b646aDcaD473523F7B32bAa74F4F8F502", + "0xDfb0348F0d8200Ee2eb50aa4755F690dC3165B52", + "0x5fb92b742BF451Cf55dbC51781A621bE9Fd225b4", + "0x501C2CAe71D9568da1972b03f27eD2D527D20aC1", + "0xe32b9ED080d3051Ab4F291d9F65D7528d139ffB4", + "0x5750D70f57658b8A28a8ae659c3f8020B9D4117f", + "0xEAD67D6463cc332507c4B321B022E84e5FbCAC97", + "0x44af58E9ee170bff3f09B224110620DCE521Dbf7", + "0x2aA6b66cE9432a67953D1F37533CF0B05A5F6e08", + "0x03F22215ebbD72FBB98b15d39FCDbbe6BC7b9A8a", + "0xC7f56d386D42151018C6dd0AD08efDf6240A618e", + "0x35E8171BB85a471C8e6B379C19515006dABFf236", + "0x4534b2B160217849776B1885e082b0e39Ec7dF08", + "0xa0Fa13F542c1f45fAcc537F8Ba8db3c4F91a4fD9", + "0xC9478772653fC70f52e60c529Aa9de99f4ef95e2", + "0xB005fc539BBF699Cf72af82343e0da5dA5fcffDe", + "0x70B31325eC1EEd59e2FEfC4076F08C06C913940a", + "0x041Da975e077596370DCFd6E67CF546e2C1bedCf", + "0x1Ec583d32ce166A93B4E87f35c1bbf99017f4029", + "0xF848C171f708C45B28Ab299Cd58BD239B9836939", + "0x9b3E84714e7b8501648B50e2fC002f8F3dEC5Ad5", + "0xB489400A5237A7cdB38DA63AB3EeD973a968529D", + "0x9bf6c52D4daC02590D6e621F5edDCaa716eAA23a", + "0x352D68686d21237582566c6b95DD7edbF04314dA", + "0xa55b524DbdD2D7080FaaDAe7807CbA904eF8A827", + "0xba0BD4FE75d076Ef758131BCc15f435C45ECb227", + "0x491d927Fa6c49460446346cc93e7B5E75B0B8c7B", + "0x9c4b88fBE06b6B1E461d9D294ecB7FB0c841dB4f", + "0xedF5025c84231784293970a5b8A1aC9D29f9b758", + "0x5e334FC2eEc978478E84d17446d842bBd8C5Af7D", + "0x3452c9A1a2f670FBB4eB6bdf91C1eF4E752569aF", + "0x6a4Bf9162F59339162530D2fd576a2604212E9f9", + "0x04Cd750e3e1D4BFc62c4e4A972ea3580a68a51DF", + "0x5458210BF50ED1864639F45B5AE7Df76Be2177e4", + "0x36d481b3c6ceca782b1FE72a80D704dA06E6c063", + "0x56FED5Bb34d8ecAe854A58502beFe1E1FEd679c0", + "0x49a3Aab8EBe80725646937b9E9f6f8b4e9867bfe", + "0xd99591A00a7Ad4B5c1Bc560eDD219f7749aCf2D2", + "0xbA208F8Ba2fa377dfa9baE58A561D503C3F4d96C", + "0x84d0F74d21a89F86b67e9a38d8559d0b4e10F12d", + "0xe55C38718aaA0e777F7c6057070a89134418c67F", + "0x8Ef63b525fceF7f8662D98F77f5C9A86ae7dFE09", + "0xAaEe61B58DC32E7331104F7A51d2A3220edf4261", + "0x2D7cb9d0F2cF122fD8768c9c7dc30A611243e00D", + "0x3EBbdD78edB895d8946181E626eaDb49d58E62f8", + "0x3DF5d15722E8bF01AF9Fd19c2e2e5Fd3934357F6", + "0x82767B4b962A2C8d0F60df6e4797D77aFb108904", + "0x4668B69ec8a429597Da7530a8e328f6CFB96D71F", + "0x106BF19D7f44dFd1e4D41b0890E568Ad7Fb5e511", + "0xefCB1b77e1f14F6B8282535B96Ce61B6eA99F1a8", + "0x53811bEa240EDCCDD922dB6733E5F13bCDE1a6eD", + "0x6F6830BA0571887fE2B5Ce0bE27814Fffc4029DA", + "0x56e08bABb8bf928bD8571D2a2a78235ae57AE5Bd", + "0x0de4348c28c4e9eF19d5Faf4024Ef6653c326ec8", + "0xA7eeA325c4131396b5ec63a175C9d00bD45a5098", + "0xD3d20F2D89922311F87Cd954691102f475833770", + "0x0d0C4D8bc5029E8420E1Fab78D1373eA175F4bAE", + "0xB8cc0AE73c5Ee88Ec33Bde0e3795F0667Bd46D19", + "0x1012e83Da77E283910D00E9Dc1Af5B103d62db12", + "0x0ae43D62AB68d0028B54eb67457B7c013d4bf6D1", + "0xf1B1Ac4CC89b151DE017e06ed7D82436505a6056", + "0x78b127298fFa031F41D1a7b55d1cD6Bf02A272A6", + "0xae4EEf02E0eE91061115fdF580d725D1544a2922", + "0x63B0A55c72ebebeaA528FE28E7971C6195C7469b", + "0x94C4b962B5Ad65Aa670FEC3ae6D6c7b27f8a11F0", + "0x3bE960F410100916b49C41bb8fC52eB5511ef307", + "0xBdee80688Ca00e6f89dAF1B8CC76Af0D0cE9EA45", + "0xE0Fab13e3A5fc6d7BEcd6fbc93E36250375491c6", + "0x2Cf638A9465B34408847C69AEa170F2d2f5E3153", + "0x9AEF7C447F6BC8D010B22afF52d5b67785ED942C", + "0xDBB3028717E692ef4C002cC7019AE7192bF582C6", + "0xE1518a5ae4C762a0Dd7773B987F6BD8bd7cA155c", + "0x1623273D676D1eA1C4Cb5A85F2CA5f1c06C2de93", + "0x612b8E8762BF14F445467d023F9f04c0C22d3DBB", + "0x3B2dADFAb8Fc9859d2aaa9Ad09Cafa2B179f67BC", + "0xFE2fb8587760c8d5960CB7A5BA2f2299EdF10506", + "0xC29dac18Bf481C1615caEbea293B84C15B383828", + "0x1aDcF07389b1F6605C44a7683c50A5243829A92C", + "0xB92246136f049607423B4C986302D407Aef91A17", + "0xCE1CB8BB004D6f3b9f8166E3fb90617C5b723A70", + "0x97F16b00d436FcD49D5911e68002F1cd4d5e47c5", + "0x710969f07f9B94428a612e26dCdF7728524B01D1", + "0x7AFDd1bA1145e583FBa788Cc21dA686E736A311C", + "0x69DA58fFf827b1Cf91677b72B8fC515f61784762", + "0x205043ddfa3F742bcca920332bA1e6970701cfcE", + "0x7569Ff833FF6C5fDbb58934862A08281828673Ea", + "0xDe5634b9199Ca0Ca036Fec876FC249043dc98fb5", + "0x750f53f8Eb3024Bd63913480a0b47F3E33Fefbaf", + "0xf89FbC24FE728b699a6c7D05605e2a4b944c6E39", + "0x798031C6b05A37C4891E6928B1C85fcF153f5D70", + "0x59D274BA41a55827062E2E4aD115D8FF758F391b", + "0xe51F6b8b148bEE49C8A7ba87cE088d7081EF6912", + "0xF5EDFCDE8B5e43EcfE382Fd5855612b3bf611224", + "0xCB966829e34f1b376715471398ba3E1fa50681C2", + "0x5fc026Ab7F7C6ac62c62e4382F7FF3d37e2C2a75", + "0x84dE7a37F1d1c05aaa1502f87e4330adFC9CD32B", + "0x2e060833573A11E2A06Ca467F12DC99AA04FDde1", + "0x5e7a0aFc7e3C32f844114d3Cb375ab0C456A57Ae", + "0x5a0a4e95f30FAcd178c396199a9f37b6c9ef6b81", + "0x3e763998E3c70B15347D68dC93a9CA021385675d", + "0x64D196E9Cc62eC70Ca0379DfC38CCBff344dD38B", + "0xDa10F11ba76503a2d9E3F29d4b92b6Dc3417bbeE", + "0x33606226e70B96cF2a07E85401A70661adc3D019", + "0xBEa645Da9c7f6De1f7E287759CcBFd5ab9f57C08", + "0x4f555e32E41c6f646ca72A93130fD24be10A8016", + "0xf6f40CeD4Cb39ed9Eb2c2857b8cD0CCc2dDAC241", + "0xBfb12c30a347d06778Fe9DD582C33c6deDB373ef", + "0x02802e1aE8da338C8a5d220D857D27c49Fa86A81", + "0x5761774922b95084454c8720D647250bAD207EA2", + "0xB46782AB70619B1BC39e1342E9E77921674e4A33", + "0xeC8804867a84cFB397debAD4e1BfFb1F015bd7E1", + "0x8842F97d36913C09d640EB0e187260429E87d78A", + "0x01ebce016681D076667BDb823EBE1f76830DA6Fa", + "0x8a146531630850fE1d158c922bDA620BDDF12766", + "0xee1cd2CD8906A5e03b879FD1a05E9D08e592125C", + "0x3d2B23962EBCc882f9f65452658BBBa9Fa72d170", + "0x35e45b6421c01a131FA42A00f34091C3007a952f", + "0xb14d56ce29dADE0316EA5Df06D491636a7c4F3C8", + "0x4C71e10fBb6feBB14125EF5bF100A2104F5c1Ad2", + "0x58C4f03a954e4CbB1b8E204a881a8e9A99d015Dd", + "0x8234AC0EeE3B4b77EF94E4cf2681107d19025562", + "0x193Db18A5EF9a0320b7374C1fE8Af976235f3211", + "0x469F25C297b2E84898cfB1CE596f2553285f3E9b", + "0xdBBe2bb7be24319808E01DDf1dDD9276Ad2556F3", + "0xaeE5CB72Da840cF679ebec780e31295b6BfC746D", + "0x9ee30B8D06017d579752c19FEEe719D2aaf59265", + "0x7ECe13F2fEE7f2fF9Cfb56b1F6d1EF2A787D68D7", + "0xDA5d647aa9cFA75167D0A9F299fDf01beb9Bdd1f", + "0x23c043a293b302E146Aa9fcf9a9CA937b895dd56", + "0xFC19EdDC148e92a59C67d1DED4bd7f27aFeA0194", + "0x92Ff028954F615c00aC555f2E852D0E1b85Eb5b3", + "0x79e21f66b9813f7922fd56E14051bBbA83EEf96D", + "0x1e2CC37406F868b3d481170248e8aB2fb0181644", + "0x291f1593C2bA68974bC6E0AE715b52ee313813A6", + "0x54BDb287edd2b9FBa7cbBa098783bd14B19DB93d", + "0x6fE4aceD57AE0b50D14229F3d40617C8b7d2F2E1", + "0x2A9C628F9B1b06a881407AE457Ffb0E51FB7E752", + "0xbe61858c817A6A17774F8079A5Fbf9D1896f3736", + "0x8F25818ff64Be1abee91ABca70c306B33b602062", + "0x942c9a79866EbF5D6F24e6b308820541dA6709c6", + "0x645C22593c232Ae78a7eCbaC93b38cbaC535ef12", + "0xFe202706E36F31aFBaf4b4543C2A8bBa4ddB2deE", + "0x755CE5F31617deD6e41D15Cc51D8FF642CE10e9d", + "0xCA9c7E1c9BB058d92f1ace6A389857eEBe7b1368", + "0x906f186f2398949FF16183D1Ccd47796d75204C1", + "0x5Ab8E8a5Ac6Ee75BDf3ae1E9Bb5E354232768053", + "0x49Caf2309CBaFDFA4AB28D11aE18C3Ec9b1Cdde0", + "0xcbebeF2DBAFeC16529277bE933d60CB0524aDe9A", + "0x0d8C573e3938713c02cCC77D569eB0E5bc4Bb20d", + "0x3029087FDE42070F36e779467d0780B8a587AFC6", + "0x264a78Ac40c8F6EB8B3f163DC6E0CC5A1FE9A100", + "0x9390537C031B3f53c5982A8F40821602fFE3B28e", + "0xf9DF78D070F24c644D35a56a8F2971b590084e9B", + "0x75576e0865f7e517DFe6917C03d70037EEce3Ba8", + "0x827C4243634e99B146c9FC73Cf703af5e5D183CC", + "0x8D32b6272a22deFe432100A57D952f7e8533692b", + "0x6c3C301bB3AF46c86205844c7Ad9eF8bD6593BaF", + "0x9f4d78366F3D67AbC8A69B44823e4ecB8703ebD3", + "0xFe9B6965c27f24F90180054175e07D4f9b3AaB8C", + "0x927154b2A5B19638c2A452e8F74e0B496C2125B7", + "0x6A1Dc2f320AB982637Dc4b67de1C0cFc4Ad988B1", + "0x07e6ae8F553DC77B8b372e4d20dAb797475E6119", + "0xe6FF466b9E9259DCd31A9847DC11872d6b066f20", + "0x001d0bf28a03dbd38a92cA4ACC6c4c7008C952D8", + "0xe052b423cbc4e0103Ecc7FAFB0ec6C0A911Df40C", + "0x14Ce0d3074E93f2761810fc64183A2bA9fC33481", + "0x3a4fa1bFC8EBb0C958C4CB6c0B72D53C3aCDEce7", + "0x7C11a14D2010e3a0A93b6291449ccE10b2e38f66", + "0x023aB8e20a4682d315Daef4c91DB96bD77934D66", + "0xD9Af06CA2eB971f818f0c41D40e5B8f89830d7CC", + "0xFe407097E296C521c2BEAA5e4E61A9b91bbe0D61", + "0x6FB2775c1424B5B10bD810bE11f68274264a7E48", + "0x03FfdB95D3eE7F7b796069867E55CF6fdB65472C", + "0xF3E3F3032Ef4765DD523D5eA2CE23c36c24dCf36", + "0x3b1b211B3FCe0D1EE9f5e1aFeecadeB98378d0b0", + "0xc0A736fc46578a9D0CB5595f19035DA934755caC", + "0x98B5532FF6201D9bB679EEE90cC34C10137998F4", + "0x0952222D50584a63Fbe38D5B4da94b06Db102d46", + "0x589fFAeAB4A99275660a9fa4274cb58F7329f4Df", + "0x9050c755691E9089E08aDee29462BaB6b338def0", + "0x8074Ed7AeA09a7F371aF44daBD4De6C42839929B", + "0x7A921b0365B4571beE00ba5A1DbB422B11f13F9e", + "0xb119efDaBF71E3B38CDFA393Af50e972a1149e89", + "0x85D38bA18634f64bdA3BeB3518DdF41796ca2024", + "0x283A2549b640E56901A8Ced2Ec5277A32CfE6425", + "0x80b744588fd2feA8ef14386bd953493A097eadb0", + "0xD77ee49BAD638Da26775779e830339dc3F61edE1", + "0xEb3aeeC75F1C6F0E3525c0e9d7ebD5ed37A2B8FB", + "0xa0F39111d7D1124ACa553e53883ea4d991a80675", + "0x083E952464B4a8e988ABE39868a652f9C2AD050e", + "0xfB17d5CD85854B6Bee89e714591DE521F3169dE5", + "0xA38F91AEAf08a8E8a9e488DCED678099d688900e", + "0xd1F4cF428835a775916Dd767e11553b5649EAF87", + "0xD0FE971732E547059eA6009c79a2d6155bdb41a4", + "0x7DA220633dFaA2dD4eF3e3B222e836a2E23A496b", + "0x0442F45807CF2Fe3a3dAEd99762cdD9cEfF853B8", + "0x82e18803b9369C8210390F70cd0237480f7EB1EC", + "0x3A51C9CFC601E6192d24A33dffF394Ee8DaaA144", + "0xe13a47718B9bfD9F74008068787827af3E5931B5", + "0xB1bD394C25F653b71F56F66DF4a54653dcBcacA2", + "0xB04f18452bb410eB664C03498a481a3c04D6EF76", + "0xF925cC2cbea0b2B30aef6C29ec0c77d7cb6FcB6f", + "0x2346FEF37a4F36753594916bE2aBb15d2566C4ca", + "0xF6C25Db73423ACB7249E1fCC4ea5E3f7f1eAb4Df", + "0xadD2975595fD251587844D9E0FBc8A31341D2a27", + "0x28aa4F9ffe21365473B64C161b566C3CdeAD0108", + "0xDC20107902Aa222658a135E6f61F905f47998387", + "0xD638dC334f3A9b6692F7Aa71a32949F7da12F9D4", + "0x5f4d787692b0ab325DF37E0b999C452d4561A8e6", + "0xA4264AA379aD275aFaD55b77d22dE421bF425280", + "0xe93F324C0Dd0f9C558B9A4652927E5D06cBe85b3", + "0x69b8A24edc9053b3D007710E7B986dF40a0BC7eb", + "0x838998F5EC821aBf63F17C72025E5d2E277344F5", + "0x8a806Bc475331F20022D897E4f9DC066Bf0324c8", + "0x1B647EA4993946E00DBc9915F76d88212b51A2f8", + "0xfD20125Df89e8Ffe237b5bcA38acdf3cC54Ad5b4", + "0x54B325783095132A996027a622a5B4Cadc8bE876", + "0xCE5B6d4F24b06F9c93c42653695A26AB88f1B951", + "0xf9AEb52bB4eF74E1987dd295E4Df326d41D0d0fF", + "0x9100FfdF05ea6898002Caf643d3BD0433ABC1d15", + "0x8Ee25bb8D6777c21cEffdeA20583856c620DCae1", + "0x9C9553CD0c8673651A36e5E11C4989bb629D4067", + "0xe0647674B89BF40247cc6D7109F3C256949EbC64", + "0xb616d066fb1aB1384D47A5a70Ce00af515445A6d", + "0xa4F967B84Ff09E0bd4368EF934499a6a3E4BEE22", + "0x0C13f7c69f1b3D05C0a007E29f03b4d948b0859d", + "0x7aAF6B8506F030f0DaC415d03C87E89Dc891EEed", + "0xEA2f509cABc86E09E8b807258D3f9A5f72F0da11", + "0xcD7d70Bb7034102Ee0C828FBB83776E0137DeA8D", + "0x21f59001e8543fbd84fCACAdE38B1A06274F0cA3", + "0xfE81028185B4A2465751F181bD8D2Fd318d460B2", + "0x2d9039cBe219420ab16d31BcB7a37B395f0cE394", + "0xB7deB9804764294d7fb5296BB81519F340247C03", + "0x37900736d4BAA4fC9930C1a89352391228628C64", + "0x64e4fc6E6e153786C129b70c4477D69c69D870a8", + "0xbEBCCD57f93b873b017457d1670E77E5F96767aA", + "0x7497B333895C64A56924B293e68a6468963f2A7e", + "0x30044b7C080A6e456386d5b9c370Dc7c259AE2cd", + "0xA05e21550eB86462388783047dA318D2de20BBda", + "0x16Ce1B15ed1278921d7Cae34Bf60a81227CFC295", + "0xC166b67C8D94C1c1F9AfDe897E8CA5d05Cd2385c", + "0x2B01501C2E2e3200E68F1FdC557a10C82Bf05626", + "0xfA9413b9F08252588B4040f888c5F758ED6D10A7", + "0xF116569b3f888D639372a5485685A6D8EE28A593", + "0xA51b7c8d109B0aeE51F8C64165Cc170354b9323B", + "0x2354910D293E778AADf39B16deAadabB5B40D66b", + "0xaae31EFb56aF2d4DA4f8B98aEE5F269e03D1fce8", + "0x5A09EdFCC6f4aa80EF9c996521B1a33650bf4c81", + "0x094791fAAAD2a4a10f27838bC354C4D7d0BD8e75", + "0x27667Ac10cA637448ed6cdAe46ebB8347bfA6b34", + "0x3bce96AB4cABd0b8514E42A84A074403413863a8", + "0x24A7e2F78c3AC4181996e415f7dBea1A71994952", + "0x0422211c02fef3F623B1dA67A116482B029D8BCa", + "0xB4773a58BFa5eB5f1029334D5dbde2F12Cd35D42", + "0x5bB872d51BA73847DCFc5031fEe0af568d2d83E3", + "0x8fCC6548DE7c4A1e5F5c196dE1b62c423E61cdaf", + "0x9D99E6102562F3465988402831b3843A923f3aA6", + "0x625Ec8189A41132EB43166929f9c8B20014f74cB", + "0x1CF8423299241F155716eb19D0ca65BEA4353F03", + "0x4e53433495619613251ed5d9360d205A3B87d047", + "0x9fD28576964041Bd73ea68F2F09EE587c858d291", + "0x13dbD75AefB4ebddea327071257948edbA2Da6E1", + "0x9B3fe4109F9Ee66F723CbEb353CEb365Ae666Ac2", + "0xe2eFa7C8A0769e91af174870DbDdC2D2500c08a3", + "0xd35E24699cb637207c7486A570039d30FAFa824C", + "0xa7DbeE7f9CCDd8c91B75067CD31cF0b6d594fbfb", + "0x1D9c68cfA5fDFb6C585bF01f715d359f3039b63A", + "0x98A529e3488c2d44566881FEAB335B17B1c3b430", + "0xe827C9485d095a82f427a4D1a54e781663DA2de0", + "0xCE91783D36925bCc121D0C63376A248a2851982A", + "0x59D2e0a2be0DB463c36038A5fd2535F58Ab3B38F", + "0x1393D846210262Af69a29587AEd52C24B0Ae6223", + "0xc4C6E038C1A126C57F540A4c5d57f8B02d05fda0", + "0x908483225afD362fEbE58F032E1CcB82002401bC", + "0x26563EEdB269448352Ba0663f2EE554FBd389296", + "0x7FFDd3AedBE63FD45570e81F7F75Fbe99463470c", + "0x50f527A8f3898BC4b26f5b032EE60C5fC906A2C9", + "0x40Ee2aFBFBc4286b25e2A194920C7d9591eef9Dc", + "0x55af604E03BEb3457DD754d2F630eedDa6A1fCef", + "0x1be8f34Da50d68a5452538Ce6c53B43DAec148be", + "0xF06a70b9248072f7F17492A962557447E0001152", + "0x3374cb587bF1bb914946B692eF7AE80f8C344091", + "0x153B4F4f2c6c284f844A53eB2e097D23B7bfa4Dd", + "0x911A182A475C0b54B6A861c77CB5A8A1e69b482b", + "0xb9806d3A651eca707DCeE20E9E82310754878fC4", + "0xb6888B2f78939aF6515D82A654C0e9a8763b8722", + "0xa0F94a872D5736471D50a14cD65C81FE5bf27C79", + "0xAA916C42e129E6344acd5c6aa5E809F19A8F765C", + "0x03f65d114DD493B129Eab94B8eD8190a464d3d5E", + "0x44A67412F3B7298dB589e456253172A4fd2EE541", + "0x0faA92aB469Db5D268fD37C2F1F5E39F72B0E3B3", + "0x9FAA6a2CD5385Fc277498254e535EB617ffD0C63", + "0x7f11BEB8887F76d2493D6C18498c27379473C85B", + "0x15E20bF6F80f6CA69Cdc6F03483e2d392860eed3", + "0xA1a249a46D9334c372540a1919e71210f1197D5a", + "0x3F7d5f190dBD3d0f68fb434efdb3267c02E6720F", + "0x1CC6722C1cA27B1B2d3D89Ee5854b4F76FcE829a", + "0xe0D01EFEe7A9740F8e702F086dd4FcaE87926Abf", + "0x76937f60b76DB3B5330B2d257138F994dC160Dc0", + "0x2eb7D6C8e738d1c9a35E7a4AB43D50418a637b7f", + "0xF5A945F4FD8633B4573Eecc61D5307B267831C90", + "0x174E070920007350fA0193E0321f9bBD3984949e", + "0x1cC3a9B765199Bb1Bea3cbA45b4A203C55ec4E1a", + "0x3A92b4BF4F6528dD12F8b0fEb4E91768F65Be781", + "0x81a9769Bbac5E53F665c0deB4EACBB0094503810", + "0x9Cd75894548a8969E924F0B3aF6bF8dC27612b71", + "0xf147E390aaBaa560C34d44aa84510858c476a677", + "0xA6A2AD1320Cec227f24f404fcCC120B71b41757d", + "0xB1e22281E1BC8Ab83Da1CB138e24aCB004B5a4ca", + "0x12F20fb5b6047F129562c97b58fb2a78cd80b765", + "0xa3615B094D2aA6D93EABb694495Cd13Efeb9916b", + "0xd56EE5Ba5A52e15f309108BDd6247C69B4F624C2", + "0x6C0d470D97ef1F230EEF10A5c848AFCc8261ed67", + "0x1Ec9Ce39b453FF37bf21ad441Fc4060EFA90c4D4", + "0x439eC0fe84620c097501d3eB6079B37204e446c9", + "0x867c55Acd5A4691d9336B26bb15c4dbEdE5C188d", + "0x58Ef5039E1c799c4C2A454faE28a2dEE21916E44", + "0x3D6991085Ab1ae3926cB96f25684C40a364B6856", + "0xd62C7913aa2B190Ba54aF680C92665c88D6F89B4", + "0x4b7292679D439CCd83B972A5fF848b47391AFfCA", + "0x94382cF5d7F6eb2dE5E5F41553086F08b14e11be", + "0xEcA88938aBfF4F6329fE99Cf67709B392DD0F7cb", + "0x901Ab2d7EBe247f2247DF9cC090f81714920E0ec", + "0x1301910A3508E3D5b5549D87073F0d1abf547991", + "0x6E2Ce0D1f8Ab9C82Be2D0beC89a29fD9AF9837c9", + "0x29DFA3B040A65ee3fC0DA515343c18cdD49c635f", + "0xaDE38Ad7cD00C20cDf60Afdd679BE32F87b7D802", + "0x5FE3971e91a40C7Db540BE68d0aF7e154f1d4f54", + "0x4b2651d278966402a6dC3F20F60b590Dd93a81E5", + "0x5Aec24AcA614bf525D26087AE41ee3f9c7B0eBEe", + "0xb9C0aBA138B98656FfEa4309bfE2881b0b7c1d96", + "0x02BebA6E034d547c685Fd496dD1EA783C355fe7D", + "0x2DF32Ef622c427Ad9A3dF6bA76b992622e2E381f", + "0x1F87eE1e1e73fcF1dAede163B3F40EEEd9DB7ebE", + "0xF9026C6FFb136cFb190b95BF617567a23452D823", + "0xA8E156Ec480a503410f94383ff6c01B074c10e53", + "0x7C6b59F52578a1bea4F8D750c5B4BB044669B5Cb", + "0x9CDb231Cd70B7522c2b43ad18240649f9599F4BE", + "0xA628114D249Ff3De888C9076A2cE370175e50617", + "0x9086FfdA7e3E34Eee95dF281D61473Bbc2d18f88", + "0xeda9CF63AEa93301b1AdcaD3C0013Fe39CaB8cd3", + "0x8322961C710571556Ec8bC91554f675CD1828E85", + "0x8EDAf13697bcc248a428DF216B42610f9AF5dF54", + "0xfc2098B31f7cd6e520836b699cDB98CB256bEEF4" + ], + "Available": [ + "24088642601929700000000", + "4013884798687270000000", + "1427271995945020000000", + "4923400486461240000000", + "565699216017207000000", + "1050972850795830000000000", + "22860265809159500000000", + "871520509864269000000", + "4413593223200000000000", + "6602315280662690000000", + "1611304315837500000000", + "17723329336693700000000", + "11543880254463600000000", + "20488947641474300000000", + "105856431752225000000", + "93551344110000000000000", + "114418266099801000000000", + "6339267167826990000000", + "404546352908108000000", + "46781279652054600000000", + "20294576261006200000000", + "10904169762141400000000", + "1124372328441090000000", + "6525229076767460000000", + "3174350247238620000000", + "3785674422987690000000", + "31824786145484300000000", + "59375060732274000000000", + "44039131016679300000000", + "1590375451308800000000", + "56624150366404000000000", + "1527050294915700000000000", + "575814141931471000000000", + "164658078624408000000000", + "724046427866970000000000", + "59547401319730400000000", + "784563374179046000000000", + "1718985616188750000000000", + "1346393890771150000000000", + "77088072125010300000000", + "252984577171841000000", + "28173702954232800000000", + "0", + "12672529542113000000000", + "2750992456034050000000", + "4143478737845200000000", + "928746209917108000000000", + "142788118763215000000000", + "35621236542900500000000", + "788865388170811000000", + "108773147542576000000000", + "2049134102901940000000", + "41377436467500000000000", + "82554674162924200000000", + "5758061256154080000000", + "110645137542310000000000", + "5336269249337900000000", + "4930973932668980000000", + "0", + "11718234684533300000000", + "3302034629882290000000", + "53014629182909200000000", + "1801573361400320000000", + "36275830366292800000", + "680431094033721000000", + "4850615368811720000000", + "108078781004553000000000", + "34294254265343800000000", + "30773222452559000000000", + "15015222843787300000000", + "238986748003075000000000", + "56538316120943200", + "8933948820673910000000", + "4168563029289580000000", + "499585386803338000000000", + "3407075238484640000000000", + "372337653222345000000000", + "421478143541406000000", + "463089461141888000000000", + "336204948097149000000000", + "1905744860928130000000", + "3282493148280520000000", + "89301806769450400000000", + "1933297571040840000", + "33248248720684300000000", + "335961632636961000000", + "18091558611882900000000", + "0", + "39214410314612400000000", + "42835777950124000000000", + "709633092784286000000000", + "299242003202194000000000", + "46604768740956200000000", + "8616444660294470000000", + "112527329541203000000000", + "10529286989025500000000", + "2159077793577170000000", + "74334591334499300000000", + "151770625676190000000000", + "139999348243681000000", + "17932576378856000000000", + "120777382121351000000", + "2531880102781820000000", + "47611604406505000000000", + "18530798502515300000000", + "1684666010080800000000", + "334407398491810000000", + "41677048966090600000000", + "8749571843959080000000", + "4640232073794480000000", + "55087034315589200000000", + "2758495764500000000000", + "84492535034905000000000", + "34633772175548800000000", + "444699737307917000000", + "0", + "6747914575116700000000", + "9980331715136730000000", + "109834243331086000000000", + "5852812942060680000000", + "2727048912784700000000", + "328314505789069000000", + "20748302140643900000000", + "18923921389153700000000", + "15762547989779700000000", + "0", + "10126327611648900000000", + "31216560743583900000000", + "275621478660415000000000", + "264681186490461000000", + "38396514295188100000000", + "982982538857953000000", + "28546493350814100000000", + "337121961632892000000000", + "4255311026900600000000", + "8285886952310630000000", + "61238227689914300000000", + "32978534287337900000000", + "1400162681606850000000", + "286298464815918000000000", + "473372782313472000000000", + "6701633275329690000000", + "1594128471902180000000", + "292471746502122000000000", + "118631599727074000000000", + "41020528194136100000000", + "345603372059020000000000", + "41615617300671800000000", + "3566619535925950000000000", + "43189689204002200000000", + "0", + "1123982045720600000000", + "12593159980195300000000", + "82969291005597900000000", + "95219158090209200000000", + "20244589158231800000000", + "0", + "16893949873324300000000", + "0", + "116779415084789000000000", + "0", + "22369245371495900000000", + "2228611303169700", + "5877520060747220000000", + "20328692385145800000000", + "15447576281200000000000", + "2909402099164420000000000", + "24457226639740700000000", + "5613080646600000000000", + "0", + "0", + "3322179143610120000000", + "9071072232969590000000", + "1216330969830820000000", + "1094862410343480000000", + "21900927562079400000000", + "74362357972469400000000", + "76542949114228800000000", + "60611885878321700000000", + "4413593223200000000000", + "148866467086687000000000", + "47927810848266600000000", + "14266162256970300000000", + "108906684858919000000000", + "101639356292888000000000", + "34149769390436600000000", + "2617952510110660000000000", + "1321435549416140000000", + "58017875634191700000000", + "22730626677199200000000", + "3107774810270030000000", + "880988610495138000000", + "263281414414537000000", + "3102222413754190000000000", + "9896487157363650000000", + "766342772799194000000", + "17600787286968600000000", + "0", + "5805699003800880000000", + "1103398305800000000000", + "140352929892287000000000", + "33712196075675700000000", + "4485461892041390000000", + "0", + "19030391638045500000000", + "74577081407035400000", + "2275980528732120000000", + "1271244273955960000000000", + "88384916250289100000000", + "52711726085449700000", + "2478684148059030000000000", + "41253564968615000000000", + "8670484616998950000000", + "753391214112001000000", + "1851994466344500000000", + "0", + "11628523031871100000000", + "87780478199674400000000", + "25062255755700000000000", + "0", + "1781842931547050000000", + "786573524228323000000", + "29598778235304100000000", + "14968215057600000000000", + "1655059432151480000000", + "3168238195527900000000", + "40202561246068400000000", + "131918420852024000000000", + "7247259019346760000000", + "107737719409414000000000", + "73682183413849700000000", + "2762625840152820000000", + "67786550452785500000000", + "33209161867610300000000", + "12182336328712700000000", + "96197074328258700000000", + "103202771208286000000000", + "246815719201231000000000", + "9149475147496690000000", + "3907878384912320000000000", + "6950390109535120000000", + "63164899779223400000000", + "41302593865209600000000", + "8621056337215880000000", + "124054114943046000000000", + "2785205436063240000000", + "11800539239200300000000", + "120070784230829000000000", + "237777577292209000000000", + "14425510705819500000000", + "51830254294903700000000", + "8578381843859420000000", + "177243033492143000000000", + "4085083236793990000000000", + "1391680038032650000000", + "135543136015007000000000", + "11226161293200000000000", + "1241067931330440000000", + "19217317107076200000000", + "177036355080736000000000", + "8399869738398540000000", + "111056930117748000000000", + "0", + "402969609545369000000000", + "1149457875219370000000", + "0", + "5014880734125350000000", + "3454828367324260000000", + "6367067079347330000", + "4238195636619460000000", + "29750376820132500000000", + "5151166185422080000000", + "2909402099164420000000000", + "63511906375011100000000", + "13379079542521000000000", + "42885127545212000000000", + "3750373554422920000000", + "177440486313685000000000", + "3628060891875000000000000", + "8861801571017660000000", + "11608931927484600000000", + "6294087852604680000000", + "0", + "139880281183807000000000", + "108792549066083000000000", + "168900880485534000000000", + "146552123729600000000000", + "175173431817848000000", + "756879199814000000000000", + "16668101089553200000000", + "11423714762203500000000", + "37743394689708900000000", + "2709507925774800000000", + "128047223560434000", + "5515801142528230000000", + "42508492966918100000000", + "8762155339739080000000", + "172954072916277000000000", + "9932915823009380000000", + "25970759532457600000000", + "216311546925898000000000", + "0", + "10602485665800000000", + "30343475075277400000000", + "103909360425662000000000", + "2033756172292920000000", + "996666715837819000000000", + "87393343794551000000000", + "116998114719940000000", + "115400699114051000000000", + "5735580498348410000000", + "2485705731978240000000", + "87330021557646900000", + "23771902874190100000000", + "4269934428752100000000", + "6733936991088720000", + "189001942601394000000000", + "23863111755700700000000", + "2042959082185950000000", + "118283810654331000000000", + "13484878430270300000", + "2974847393597380000000", + "63247897000361400000000", + "20100573451382800000000", + "29380826968074400000000", + "21308537281240500000000", + "3167378718673160000000", + "97309325605096000000000", + "330917147101019000000000", + "18191421309341000000000", + "9339485898073720000000", + "36445873390924100000", + "0", + "0", + "4587515163153880000000000", + "6248635277046640000000", + "0", + "11595543299043700000000", + "87306471808830700000", + "108140990718061000000000", + "0", + "244839525922229000000000", + "26485198856737900000000", + "93706112341160700000000", + "4093288417193520000000", + "1826933268278630000000", + "31727621966215800000000", + "2598268918741230000000000", + "103916423808888000000000", + "137639145073416000000000", + "116950944093890000000000", + "32586671209230800000000", + "10927155049242900000000", + "20330597187170400000000", + "118498369206000000000000", + "4855495152873640000000", + "0", + "28314442914342700000000", + "6758690237225820000000", + "441359322320000000000000", + "135307625471539000000000", + "123807374310284000000000", + "118498369206000000000000", + "31132043868496300000000", + "30089325503653400000000", + "16764752912776000000000", + "150534636946320000000000", + "213878988826899000000000", + "21156650317724300000000", + "14177960459979000000000", + "78578607059630300000000", + "3197537446589480000000", + "623675627400000000000", + "19977562088223700000000", + "566193950263240000000000", + "414218733301934000000000", + "16769461505475200000000", + "5516991529000000000000", + "2560936861229880000000", + "4145732130925890000000", + "94685905467844900000000", + "989000931209480000000", + "4359145017714670000000", + "20705179634139600000000", + "189040818139491000", + "30102468373021800000000", + "97443696296615500000000", + "7437206240716510000000", + "619828448148000000000000", + "458717861762611000000", + "5460046094798400000000", + "15876456448252100000000", + "0", + "62367562740000000000", + "4882773486907200000000", + "59782402187610800000000", + "56741584537930000000000", + "1244851039225290000000000", + "0", + "8163489555714540000000", + "73296571200977100000000", + "0", + "15348751087968800000000", + "2089704255183530000000", + "79876380705063700000000", + "3184863620497910000000", + "0", + "0", + "0", + "0", + "5170797490397870000000", + "726123903550606000000", + "7668836838172590000000", + "23886036047548300000", + "107370762436332000000000", + "134046375699461000000000", + "244288974700026000000000", + "1915765998377550000000", + "60701162352704100000000", + "530235470031585000000000", + "2760036019587230000000000", + "1867432787334870000000", + "3815705450175260000000", + "91513959963447400000000", + "13086983515581600000000", + "733472616336446000000000", + "24146417262540800000000", + "3010413407466750000000000", + "0", + "361352522085377000000000", + "8404486757172640000000", + "21516757418197000000000", + "427836876660979000000000", + "44448730144654500000000", + "462818392845554000000000", + "25322991118110000000000", + "2890290787317720000000", + "47230768421157200000000", + "52854397528472100000000", + "250970406239765000000", + "138051687889640000000000", + "9693162569062110000000", + "20554178509366500000000", + "413064291555381000000000", + "168030694870841000000000", + "18512937102426400000000", + "1677596710740750000000", + "3406945189686620000000", + "5717727865576500000000", + "0", + "40535739239168500000000", + "15310781442196600000000", + "161122799773700000000000", + "2270473595840660000000", + "2055336969614260000000000", + "9527138103705070000000", + "2874081232258060000", + "14641417807794300000000", + "13815729863386800000000", + "2897942078968040000000", + "32678929338330500000000", + "4719707450594590000000", + "4595178863351950000000", + "181925888793922000000000", + "44602631707175200000000", + "0", + "2319619466396040000000", + "1501429162236390000000", + "5283851414926280000000000", + "7080713591296290000000", + "2240293400748590000000000", + "155778727441359000000000", + "49193609366080100000000", + "656644721180039000000000", + "152552075991877000000000", + "103665915936931000000000", + "0", + "1177709085530680000000000", + "647437628070758000000000", + "67402262091869200000000", + "82342167871210300000000", + "2267365382999500000000", + "497225154003423000000000", + "1329091785462440000000", + "146001449155250000000000", + "4509951345441450000000", + "362840753042739000000", + "225092809229758000000000", + "67221528842072000000000", + "464045686332740000000", + "0", + "8574693956652880000000", + "670486333472340000000000", + "0", + "2334790815072800000000", + "0", + "59029517804540300000000", + "132144599756815000000000", + "153862511349028000000000", + "11693881280643000000000", + "0", + "57903949218111900000000", + "3414699755345980000000", + "6417358094545710000000", + "55575822529979000000000", + "2238110431808110000000", + "8003218607368190000000", + "70709209729438100000000", + "2358367707157810000000000", + "6144481509093220000000", + "41609166948708700000", + "47088819587517500", + "307955409215662000000000", + "605566703787214000000000", + "19026196761469100000000", + "1869503490622440000000000", + "25693172931691600000000", + "7728375931562630000000", + "890001976397838000000", + "16030916983366000000000", + "66277048896055500000000", + "3558356529482180000000", + "0", + "958667216652177000000", + "99417919518572700000000", + "6237739862616100000000", + "41806840332245700000000", + "5516991529000000000", + "9159239661691620000000", + "693300680726532000000", + "57277145397850400000000", + "5045812374935730000000", + "0", + "4597801668450600000000", + "8686064400485200000000", + "325795610811556000000000", + "298457711206604000000000", + "0", + "0", + "2166048860059500000000", + "60229200324076400000000", + "5218243034178980000000", + "8631621928160100000000", + "24304101265732100000000", + "0", + "6677175238744580000000", + "3476958916542650000000", + "33548138831219600000000", + "30284073672737400000000", + "0", + "137046078112563000000000", + "180420962352139000000000", + "1207557564293390000000", + "152134916100106000000000", + "82085453198239300000000", + "15086586308956200000000", + "17989302669366400000000", + "74301299340506800000000", + "1379247882250000000000", + "10672989700775100000000", + "143741207033173000000000", + "1932662793532890000000", + "700833119289612000000000", + "6302130727043660000000", + "436318663517496000000", + "7521779433770290000000", + "63324542632398200000000", + "542335112537305000000000", + "1106203630048560000000", + "1314530300251970000000000", + "1248578289847130000000", + "346874741542099000000000", + "321227023455753000000000", + "76743755439844100000000", + "10540118937269400000000000", + "0", + "4636152221990680000000", + "8218627115030320000000", + "5911525294404440000000", + "0", + "58958207760280200000000", + "67495373958634400000000", + "25628929349410000000000", + "0", + "41462108300513800000000", + "1939268824151900000000", + "29898938166674500000000", + "23663914854659800000000", + "10910420738298800000000", + "18673284224398500000", + "223065763551343000000000", + "255147848852009000000", + "7711103696993470000000", + "281513658805107000000000", + "232724281550179000000000", + "7530230794001360000000", + "62820404900654900000000", + "40281910132973800000000", + "551699152900000000000", + "55089072045219900000000", + "7317160501695160000000", + "10761297176744600000000", + "3450841555014860000000", + "3360233647776120000000", + "2453695529650880000000", + "63757361830922500000000", + "2719727715917840000000", + "40678788853989400000000", + "13963868510620700000000", + "195892582467667000000000", + "0", + "57978897639680500000000", + "27050666131122200000000", + "24890966651349300000000", + "5556913405496800000000", + "133465040612635000000", + "766520138646937000000", + "254584453923253000000", + "1306732922840100000000000", + "9215333791211420000000", + "56384777367857500000000", + "37633754933653600000000", + "10857607824309700000000", + "36358271148613500000000", + "0", + "20617989420098800000000", + "140290778350807000000000", + "29487166275538500000000", + "572995205396250000000000", + "1530490881737310000000", + "19626288016197800000000", + "69763795277889100000000", + "116743639468912000000000", + "1726063321587050000000", + "537589152403100000000", + "3758292123854260000000", + "900465634146997000000000", + "9934186523353710000000", + "4817104553381840000000", + "3357655400176220000000", + "709431026167500000000", + "415167366214257000000", + "13720893192631300000000", + "13176166038595100000000", + "43764794410311600000", + "496529237610000000000", + "228265423923561000000000", + "7258008897629410000000", + "160672704290683000000000", + "62215154972109700000000", + "5479356497167540000000", + "276469909500170000000", + "3160135289112300000000", + "56958399714215400000000", + "11847448988134600000000", + "316530397397200", + "7930134903943520000000000", + "439522164140843000000000", + "62642509572441500000000", + "4642552777951680000000", + "494123055049631000000000", + "18002325707379100000000", + "0", + "33744584517779600000000", + "0", + "5021765620317800000000", + "6771410756017490000000", + "2683045248180470000000", + "551975002476450000000000", + "189106287284016000000000", + "2483089998460740000000", + "118928921343591000000000", + "354264735982082000000000", + "1247351254800000000000000", + "147588013048442000000000", + "40076584307330900000000", + "1481412296011410000000", + "61250360574331700000", + "4569872748776970000000", + "4404188788835030000000000", + "182877076123405000000000", + "2712713337957580000000", + "0", + "7496096215900450000000", + "108196425586330000000000", + "67614797897081600000000", + "35123622593951300000000", + "17590720808092200000000", + "376283677271250000000000", + "19868241143392600000000", + "0", + "1699152113577330000000", + "52002041917772900000000", + "26048456354742000000000", + "23851536551670100000000", + "77685948751583400000000", + "99585879846933700000000", + "3456047885170630000000", + "260934453702493000000000", + "0", + "110284363758587000000000", + "9355134411000000000000", + "1012284725145800000000", + "3064438709745860000000", + "5441039668007030000000", + "7851935249163770000000", + "109161925533307000000000", + "0", + "11267143896183100000000", + "4679139621075990000000", + "17631549019466700000000", + "1248606083393260000000", + "785163046948008000000", + "497078663329668000000", + "537167877373048000000", + "50408487116633800000000", + "72712313393497800000000", + "472635698997054000", + "30339948396869700000000", + "0", + "41377436467500000000", + "6620389834800000000000", + "3603793106619780000000", + "0", + "75581207624785500000000", + "62370246764068100000000", + "0", + "4301182051405710000000", + "38469952873549600000000", + "560971621803620000000", + "5190913829028880000000000", + "4629686143798690000000000", + "11083970096246600000000", + "39763068994925500000000", + "0", + "20949744620196400000000", + "0", + "0", + "0", + "10032630514761100000000", + "745699775464738000000000", + "0", + "17711011865310600000000", + "1662132116583680000000", + "21546982669019200000000", + "10075310500300000000000", + "10062991468376900000000", + "236745570164928000000000", + "233996243923637000000000", + "365281825563161000000000", + "1114626828757660000000", + "33853980832512000000000", + "0", + "311837813700000000000000", + "9750476154452590000000", + "11591530259532900000000", + "683309704436992000000", + "7251475167310520000000", + "343619955210112000000", + "68437218614349800000000", + "0", + "0", + "47258709202126200000000", + "635095463962877000000", + "5504368172469050000000", + "161118348426052000000000", + "5335775079605430000000", + "0", + "47706376969198000000000", + "6125432000199170000000", + "149107879162162000000000", + "6470842294967830000000", + "4193532551074860000000", + "2725708179039980000000", + "2909402099164420000000000", + "68933106279590100000000", + "2439018924335660000000000", + "11229847434150800000000", + "0", + "2083498339228890000000", + "8358774383481750000000", + "45019594193004400000000", + "3008290099693690000000000", + "124290376395421000000000", + "6346816423435040000000", + "139348527974419000000000", + "177733844538762000000", + "2583100386752820000000", + "18650933713748700000000", + "52291596102134300000000", + "35542793994174300000000", + "288837525341637000000000", + "12093247272125100000000", + "279999999697448000000000", + "69248118232244800000000", + "2057611795499480000000", + "7566725060042630000000", + "5826851346679480000000", + "189958632420859000000000", + "1257441999169940000000", + "8306502164778580000000", + "3861894070300000000000", + "8731458783600000000000", + "3766166944302650000000", + "247135546239874000000000", + "6750117813773950000000", + "118656768591027000000000", + "46379507913656500000000", + "161245666330240000000000", + "7066395121143970000000", + "326939410885287000000", + "1393254454549020000000000", + "62735447275653600000000", + "0", + "402477245215322000000000", + "14146832146360200000000", + "12525748785796700000000", + "6500758775779740000000", + "158125486018951000000000", + "11872919270263600000000", + "1661134296992660000000000", + "7024701154571790000000", + "289587644026561000000000", + "775603352121724000000000", + "221417613944964000000000", + "64615053289051300000000", + "35878952366227300000000", + "583664555460000000000000", + "1987386311548830000000", + "0", + "2544671673136560000000", + "3081045980486930000000", + "513999515673778000000000", + "0", + "27251318918576300000000000", + "27157498752969200000000", + "2945589355194260000000", + "2513308256126880000000000", + "0", + "108110716599716000000000", + "13796529756035000000000", + "59173270305729600000000", + "1007150569954920000000", + "74094636632837400000000", + "942057930048386000000", + "92099048516369000000000", + "41309904396315200000000", + "15878429862297600000000", + "6213559786128670000000", + "503416719374400000000000", + "52687053013748200000", + "1193166171524210000000", + "204466313757282000000", + "939156864774516000000", + "653785161726325000000", + "12943219297299200000000", + "26963447923271100000000", + "56773536766884300000000", + "40529569737955000000000", + "27741200261187100000000", + "90966776763469400000", + "32456178538701500000000", + "17616412904992400000000", + "1991065750547680000000", + "5370277944628170000000", + "623200758989813000000000", + "351476091044834000000", + "36960000000000000000000000", + "2685487848666520000000", + "683717965791367000000", + "203820306954279000000", + "18018895435485700000000", + "443800257480574000000", + "430567642292964000000", + "623675627400000000000", + "2201122552892460000000000", + "711933003774673000000000", + "59954640388711000000000", + "4292161788683470000000", + "92894842122204400000000", + "32167028030181100000000", + "27217828055363400000000", + "20276660219257000000000", + "33765705341224100000000", + "14211573538965400000000", + "7196420051622580000000", + "5035103173238540000", + "0", + "5352597742770740000000", + "37349496000634100000000", + "10114759733694000000000", + "0", + "125390793645290000000000", + "11442932212307900000000", + "27574842047325200000000", + "104977424661145000000000", + "439429719099919000000", + "782899706560652000000000", + "22727957368226200000000", + "6235402297341530000000", + "31588176163169900000000", + "1618943456370440000000000", + "282841242906856000000000", + "623675627400000000000000", + "24988144893759100000000", + "63911969840666000000000", + "3750606761140710000000", + "269594740353681000000", + "594523498180657000000000", + "20800499749141400000000", + "164496488537501000000", + "21204971331600000000000", + "129986165862750000000000", + "0", + "3636894368310540000000000", + "1243797802439850000000", + "1236555222253430000000", + "37268456888839100000000", + "5000748842218520000000000", + "5516991529000000000000", + "24510070218194500000000", + "10569012153755200000000", + "26997393461831500000000", + "317378523451694000000000", + "7693055835617630000000", + "4241088232959340000000", + "13997528659387100000000", + "33336687944000000000000000", + "8063178679564190000000", + "40671542441210800000000", + "17835419656239700000000", + "18538093043059700000000", + "18442529688075700000000", + "868422904191896000000", + "0", + "98248528522581600000000", + "1247285812332510000000000", + "83358408212972900000000", + "331451379576926000000000", + "5590676321047540000000", + "10766285141530500000000", + "18361508301700700000000", + "7657005540706020000000", + "10614825227652900000000", + "19845142836209100000000", + "1808659319460000000000", + "2093973607745550000000000", + "1494315028945280000000", + "51093938054441600000", + "1933806067537780000000", + "20627730385271500000000", + "36686566471121700000000", + "10403491363462100000", + "1300015995345370000000000", + "24513933834222500000000", + "170607045936850000000000", + "50008923039811900000000", + "36084860024793900000000", + "6332461880828440000000", + "23835728528604600000000", + "0", + "868008146837614000000000", + "2430384261502710000000", + "8829475290051100000", + "0", + "198758124444544000000000", + "74587842817507500000000", + "42308835564903500000000", + "27398126416291600000000", + "54065914977680700000000", + "520138550252212000000000", + "36595439061748500000000", + "649540209399021000", + "57746166854367800000000", + "171614985779596000000000", + "44351508366021500000000", + "805288269736064000000000", + "67424392151351400000000", + "692690184173425000000000", + "805031767673847000000000", + "0", + "0", + "4988294448287820000000", + "49132658386042300000000", + "11166855323726400000000", + "3849062032671880000000", + "53516443489640600000000", + "25820186581658100000000", + "6399710173640000000000", + "688785864746820000000", + "872893930745320000000", + "15397326392706900000000", + "192399371921233000000000", + "231713644218000000000000", + "110317801534869000000000", + "549183566011821000000000", + "62435302866994700000000", + "3009338650153920000000", + "441009645611431000000000", + "87440305027490200000000", + "799399799598261000000", + "6409904882159680000000", + "56909285632588000000000", + "27584957645000000000000", + "601675733095054000000000", + "2386460047785010000000000", + "992120897165407000000000", + "6163161100109590000000", + "767268775591811000000000", + "420567988043811000000", + "57367761437442100000000", + "2132510610658660000000", + "10654646923203900000000", + "29795536780931000000000", + "5594572764722470000000", + "0", + "46893270767860800000000", + "6202234905032390000000", + "871138201533291000000000", + "0", + "17428418605156900000000", + "477585243602979000000000", + "20227317726314600000000", + "14471163287713900000000", + "10703653231714000000000", + "10482158025860600000000", + "0", + "108099408931003000000000", + "19385295508025800000000", + "6431450317725490000000", + "33249493167930000000000", + "87546093709663500000000", + "60632360308551800000", + "1172068573646600000000", + "102925909648829000000", + "8818517383907610000000", + "29681420831261600000000", + "217645315819050000000000", + "678451993050976000000", + "861530379886929000", + "84699739411609700000000", + "0", + "76044716290485800000000", + "102572838685266000000000", + "6227034515573540000000", + "854442954503779000000000", + "8946366845719560000000", + "11394661581846700000000", + "4869602518408980000000", + "27095108594235200000000", + "52804917873774900000000", + "4178327815431760000000", + "29821575832432400000", + "815762452681420000000", + "24116443557079600000000000", + "647925711065999000000", + "1489289568147310000000", + "7353578009524390000000", + "15425704969623800000000", + "2237473003613420000000", + "46860690151916100000000", + "10596543793083700000000", + "7986679305196730000000", + "1139740245004380000000", + "0", + "33880225382928400000000", + "131906804521306000000000", + "0", + "25500395642280400000000", + "204891956655247000000000", + "0", + "133121249255903000000", + "111009462239370000000000", + "74092336960358200000000", + "0", + "41171818556957100000", + "3389793521582190000000", + "43472162393445700000000", + "174230025208806000000000", + "62962410996398600000000", + "199869328312878000000", + "649709048630229000000", + "557856241008564000000", + "2280353692899730000000", + "0", + "14153570020634400000000000", + "4009580937701170000000", + "4263263809122940000000", + "71680113299376700000000", + "0", + "20241476527335100000000", + "35198330680493300000000", + "40911612366273700000000", + "4711792235347860000000", + "24007187554913100000000", + "45545736375416000000000", + "26097249750470200000000", + "579968268047887000000000", + "568428604967443000000000", + "8786666325069280000000", + "34659805511953500000000", + "2405601971892220000000", + "2824247635485830000000", + "0", + "55169915290000000000000", + "4881377695642260000000", + "674866421614604000000000", + "22648090163373500000000", + "5647349772105750000000", + "4474282702186100000000", + "11361492569798100000000", + "91804687990722400000000", + "10821810445189800000000", + "127586032913057000000000", + "4748462230381290000000000", + "5081544105840680000000000", + "3483725003593700000000", + "578033831580453000000000", + "1516613549471860000000", + "12090613000313000000000", + "2955587703951390000000", + "66431647602237500000000", + "1459899806234540000000", + "15613421160060900000000", + "551699152900000000000", + "195345411731964000000000", + "694862327880939000000", + "2677631265999160000000000", + "16807914065802500000000", + "27825128297290500000000", + "9816100994581990000000", + "823843425658521000000000", + "0", + "449508744821480000000000", + "8253212337835790000000", + "26190296843245600000000", + "142052051646188000000000", + "20382528641986400000000", + "1737279587294630000000", + "366646159906488000000000", + "133769187659231000000000", + "337121960756757000000", + "8089315025900530000000", + "862654894866303000000000", + "6827292171047310000000", + "502954593146370000000", + "71720889877000000000000", + "144660492537874000000000", + "21262110630747100000000", + "1226438967148610000000000", + "29821575832432400000", + "10507124271784400000000", + "438094612863659000000000", + "1866933021434270000000", + "6669120764162440000000", + "12015265365723600000000", + "0", + "10267365269000000000000000", + "497249361000000000000000", + "8852793230763510000000", + "5700073658677300000000", + "62557806498236500000000", + "58476852905876400000000", + "56301749276264500000000", + "54746875478979400000000", + "34428508851978600000000", + "36459529393105000000000", + "748410752880000000000", + "226665910497828000000000", + "2358725047629300000000", + "190201980837499000000000", + "66622443095591000000000", + "33900249802715400000000", + "93342772393302600000000", + "4509341965208960000000", + "47576346391581600000000", + "11142569962465400000000", + "214560836650147000000", + "827548729350000000000", + "7079316475236830000000", + "40828309333125800000000", + "12550548193135000000000", + "223140393076926000000000", + "23324153840095800000000", + "0", + "857378547531889000000", + "696316866836138000000000", + "205010387686959000000000", + "69767014448870500000000", + "8680485128464710000000", + "31334630701722900000000", + "22474568391687300000000", + "621276952450972000000", + "43927844405166100000000", + "35360793253669400000000", + "237317491139830000000000", + "623675627400000000000", + "425732796288115000000000", + "206810838045840000000000", + "8043874342621060000000", + "238042931908904000000000", + "110782230700061000000000", + "5936520375832220000000", + "23009934010461400000000", + "355282560821282000000", + "81549692447463500000000", + "241239562971118000000000", + "0", + "1221984416224330000000000", + "90404501125009400000000", + "5759739156276000000000", + "9283745059210230000000000", + "436535287368122000000", + "3334827738304940000000", + "19832069695685500000000", + "7274184092902040000000000", + "20611049018054100000000", + "11831827950368600000000", + "135817219471783000000000", + "52549734378127800000000", + "92385429654756200000000", + "41238442058216600000000", + "5191547821152380000000", + "50817677020074500000000", + "181698909615612000000000", + "0", + "40634798904379300000000", + "10852772252454200000", + "41397906368637400000000", + "0", + "477543412477717000000000", + "0", + "534366721513041000000", + "0", + "0", + "12798243048915500000000", + "6276963414379310000000", + "4965292376100000000000", + "21320545270648700000000", + "24835598767229900000000", + "1065369126151080000000", + "34911148484410300000000", + "278744597268628000000000", + "13126384251428100000000", + "185234251688991000000000", + "25089580990323000000000", + "262251850968558000000000", + "14114973868887700000000", + "9944912395284720000", + "2181156174370190000000", + "47201000208634300000000", + "0", + "8239517774300360000000", + "1485223775584830000000", + "1158568221090000000000", + "107503387238580000000000", + "7663183199046840000000", + "2028860796954020000000", + "5922153908104060000000000", + "275849576450000000000000", + "88589021640737300000000", + "82162015847396900000000", + "10867989716112600000000", + "11033983058000000000000", + "1980564362708480000000", + "26714715143082900000000", + "57283170957287000000", + "1073495212719060000000", + "31556394306966100000000", + "0", + "41178121085419500000000", + "0", + "79622565502832000000000", + "0", + "28389561591464300000000", + "2775789416304410000000", + "57742073584300900000000", + "13082477238239500000000", + "0", + "21081086244974800000000", + "695730310208492000000", + "586969776996768000000000", + "273669690263550000000", + "3371219607567570000000", + "95452925481154800000", + "2145135710611280000000", + "67186878315993800000000", + "3838657796592000000000", + "54820002947182600000000", + "0", + "22469042689002100000000", + "333324952483953000000", + "2689900009085770000000", + "5332067332530980000000", + "46806646245060900000000", + "321188331798819000000", + "27989850627387800000000", + "842804901891892000000000", + "15175051724273800000000", + "59725526390478700000000", + "0", + "19001970790529000000000", + "5964319070114180000000", + "4706404841219600000000", + "13384147159402400000000", + "89469155203863600000", + "224686935012569000000000", + "9573823775045300000000", + "7921118064188510000000", + "1127039169205910000000", + "17389658160308800000000", + "525829755165305000000000", + "63620463595498500000000", + "12903946187208100000000", + "917497012488163000000", + "0", + "0", + "23143988359173400000000", + "14564623548612500000000", + "0", + "3713826227325340000000", + "427083920060770000000", + "2909402099164420000000000", + "5965393163047930000000", + "1666828635550010000000", + "0", + "98682780240710400000000", + "715717819978378000000", + "13795418145507600000000", + "5765439823852850000000", + "112192991968260000000000", + "445113781551096000000000", + "6283896919871620000000", + "1031686164147290000000", + "1446575346537260000000", + "4549022714839540000000", + "92738211802076600000000", + "45135889572286000000000", + "234113887712637000000000", + "4812527162714520000000", + "924045763881019000000000", + "17831996000000000000000", + "0", + "445499573633412000000000", + "1197565806739770000000", + "838320088168542000000000", + "474918798654353000000000", + "2758309070230680000000", + "53959942136954100000000", + "4655362664294900000000", + "459300928906112000000000", + "11270118853964700000000", + "58522100492148700000000", + "9273068414413280000000", + "3779657547173460000000", + "1213639058724320000000", + "101748198089111000000000", + "43906552380097700000000", + "9732979549496000000000", + "74163268067312100000", + "3652303362764770000000", + "147803538326405000000", + "113100133618500000000000", + "2134219401197360000000", + "3628060891875000000000000", + "106162039948469000000000", + "574420029339790000000", + "29821575832432400000", + "118974145929408000000000", + "30183487811052200000000", + "0", + "0", + "2758495764500000000000", + "33254469976715200000000", + "0", + "35118650089838200000000", + "157096755808780000000000", + "244700899092545000000000", + "6080239250030900000000", + "173585420351260000000000", + "2076494452153400000000", + "8305806296609820000000", + "3382464548522960000000", + "498162771815240000000", + "58582844363693900000000", + "212084325928238000000000", + "13248222144824900000000", + "3060352798670210000000", + "4483464851212950000000", + "3371219607567570000", + "30572293671637100000000", + "0", + "38607351902104600000000", + "117972266877242000000000", + "0", + "50657868716836000000000", + "4760292915833340000000", + "6177583703817700000000", + "22116511541065300000000", + "19179823892471600000000", + "11093280720573500000000", + "50568294113513500000000", + "11991110936198700000000", + "0", + "19309470351500000000000", + "62367562740000000000000", + "1703001034876280000000", + "2256250830244480000000", + "50347989115403700000000", + "162392775001216000000000", + "225604865585414000000000", + "0", + "3756588175861420000000", + "168872311661191000000000", + "9508445473348790000000", + "13393278133411900000000", + "49725625571970900000000", + "26464199440915000000000", + "885349637532426000000", + "0", + "532849207640182000000", + "13994971478188900000000", + "3316637015206150000000", + "104366566517494000000000", + "0", + "2949142508153760000000", + "2206796994903020000000", + "86570779540312200000000", + "0", + "11449700986064300000000", + "399258383099847000000", + "502645201090091000000000", + "576432043350241000000", + "2683000703227400000000", + "2430109130535250000000", + "73329144584281600000000", + "2568645832740220000000", + "16449957088669200000000", + "47567909826813400000000", + "10703159788798300000000", + "21859829044974900000000", + "0", + "180868429148327000000000", + "71948876876004900000000", + "20497093575018400000000", + "6236928262017940000000", + "855235980495858000000000", + "224668754461738000000000", + "8223437992145110000000", + "45652039021281900000000", + "551699188183930000000", + "95235059617179600000000", + "34113165830185200000000", + "23706576961575300000000", + "524875370516281000000", + "8979599636660670000000", + "20091964060643300000000", + "18168698391743700000000", + "7324318513800240000000000", + "3164396764081920000000", + "55169915290000000000000", + "122268835855220000000000", + "73763989615444500000000", + "11171907846225000000000", + "134570364871332000000000", + "8631647255920980000000", + "604945583657267000000", + "0", + "217411492269705000000000", + "395066049686813000000000", + "18774359895389100000000", + "7079561175891890000000", + "150431366773843000000", + "56533058147722100000000", + "9678177198786450000000", + "0", + "3873004793541700000000", + "14107755825235700000000", + "567544820934000000000000", + "243542559211004000000000", + "109177143562739000000000", + "172407270353484000000000", + "12096962142869300000000", + "63190450337686900000000", + "371686097382089000000000", + "63252867348451600000000", + "13224284539082200000000", + "0", + "0", + "3945144641889750000000", + "0", + "599410871068563000000000", + "208445391142378000000000", + "156713402586954000000000", + "95649434471199700000000", + "40458906900611500000000", + "64672302116482000000000", + "11530919105737300000000", + "10884459496643900000000", + "3558625045950800", + "0", + "5584649470856220000000", + "9023379659335410000000", + "2178181479510010000000", + "214227634222945000000000", + "18867549836601800000000", + "3742053764400000000000", + "8665122206282830000000", + "2307599821380000000000", + "3962056266878050000000", + "1641881960897060000000", + "21284366066595900000000", + "336579867644273000000000", + "0", + "3365246876970250000000", + "18403007455391700000000", + "11618746778510600000000", + "17088746616896800000000", + "1697610675067840000000", + "11595728395592800000000", + "238204349493831000000000", + "114643170397382000000000", + "0", + "10357888324877100000000", + "24577598495513400000000", + "1396077042432270000000", + "6658869261752680000000", + "242562290147757000000000", + "18467065069811200000000", + "827548729350000000000", + "4415865405453250000000", + "246686460716218000000000", + "77467056736236900000000", + "1042031103210730000000", + "2909402099164420000000000", + "62498919561229700000000", + "6875905130245960000000", + "119410088509766000000000", + "0", + "88191095511788700000000", + "0", + "13134889899603500000000", + "2909402099164420000000000", + "16686996811056400000000", + "4155455861340040000000", + "17639989892724900000000", + "97013802876916700000000", + "50705339496860800000000", + "613489458024800000000", + "4609365819012110000000", + "1552460646779970000000", + "18840617028126600000000", + "0", + "48083095825158200000000", + "2884923644033240000000", + "153028789969137000000000", + "191278861162104000000000", + "2809113112498940000000", + "126576042248673000000000", + "12137381363800000000000", + "874404022888118000000000", + "23089282320921700000000", + "551699152900000000000", + "45136333108904900000000", + "33873569923653500000000", + "385291613852874000000", + "161386503498471000000000", + "94519726245531000000000", + "13359579297670800000000", + "1010757784722070000000000", + "412573818588267000000", + "102181941847258000000000", + "10828231442738700000000", + "575175324616419000000", + "5526395946610910000000", + "37990338066825600000000", + "2904936812607850000000", + "27781032225739300000000", + "0", + "51469615634434400000000", + "15980201917275600000000", + "33890998433836900000000", + "43753003738671800000000", + "76743755439844100000000", + "7038058586046910000000", + "9899448075676560000000", + "1344459099788760000000000", + "7401598730507370000000", + "1906149412229860000000", + "19508225646242600000000", + "14025211385968300000000", + "10851908657543200000000", + "7796466187674060000000", + "2909402099164420000000000", + "21687180658049300000000", + "11644764491837400000000", + "29752318498935300000000" + ] +} \ No newline at end of file From e658ada29a9aa95cb47cba6be49b2e0bd4ecc4d7 Mon Sep 17 00:00:00 2001 From: naveed Date: Tue, 6 May 2025 13:39:55 +0330 Subject: [PATCH 16/48] Enhance SymmVestingPlanInitializer --- .../vesting/SymmVestingPlanInitializer.sol | 222 +++++++++++------- 1 file changed, 142 insertions(+), 80 deletions(-) diff --git a/contracts/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol index a506a6a..372f3aa 100644 --- a/contracts/vesting/SymmVestingPlanInitializer.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -1,85 +1,147 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.8.18; +pragma solidity ^0.8.18; import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; -import {IVesting} from "./interfaces/IVesting.sol"; - -contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable{ - - error MismatchedArrays(); - error ZeroAmount(); - error exceededMaxSymmAmount(uint256 exceededAmont, uint256 MaxVestedSymm); - - bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); - bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); - bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); - - uint256 public constant TOTAL_DAYS = 180 days; - uint256 public constant PENALTY_PER_DAY = 25e16; // 0.25e18 - - uint256 public immutable LAUNCH_DAY; - uint256 public immutable MAX_VESTED_SYMM; - address public immutable SYMM_ADDRESS; - address public immutable SYMM_VESTING_ADDRESS; - - uint256 public initiatableAmountsSum = 0; - mapping(address=>uint256) public initiatableAmount; // user => amount //TODO: Can be renamed to pendingVestingPlan - mapping(address=>uint256) public userVestedAmount; // user => vested amount - - constructor(address admin, address _symmAddress, address _symmVestingAddress, uint256 _totalInitiatableSYMM, uint256 launchTimestamp){ - _grantRole(DEFAULT_ADMIN_ROLE, admin); - _grantRole(SETTER_ROLE, admin); - _grantRole(PAUSER_ROLE, admin); - _grantRole(UNPAUSER_ROLE, admin); - - SYMM_ADDRESS = _symmAddress; - SYMM_VESTING_ADDRESS = _symmVestingAddress; - LAUNCH_DAY = (launchTimestamp / 1 days) * 1 days; - MAX_VESTED_SYMM = _totalInitiatableSYMM; - } - - function setInitiatableVestingAmount(address[] memory users, uint256[] memory amounts) external onlyRole(SETTER_ROLE) { - if(users.length != amounts.length) - revert MismatchedArrays(); - - for(uint32 i=0; i MAX_VESTED_SYMM) revert exceededMaxSymmAmount(initiatableAmountsSum, MAX_VESTED_SYMM); - initiatableAmount[users[i]] = amounts[i]; - } - } - - function initiateVestingPlan() external whenNotPaused { - //TODO: custom error for checking whether launchDay is reached or not is not gas efficient due to underflow in getEndTime when launchTime is not reached - if(initiatableAmount[msg.sender] == 0) revert ZeroAmount(); - address[] memory users = new address[](1); - uint256[] memory amounts = new uint256[](1); - users[0] = msg.sender; - amounts[0] = initiatableAmount[msg.sender]; - IVesting(SYMM_VESTING_ADDRESS).setupVestingPlans( - SYMM_ADDRESS, - block.timestamp, - getEndTime(), - users, - amounts - ); - userVestedAmount[msg.sender] += initiatableAmount[msg.sender]; - initiatableAmount[msg.sender] = 0; - } - - function pause() external onlyRole(PAUSER_ROLE) { - _pause(); - } - - function unpause() external onlyRole(UNPAUSER_ROLE) { - _unpause(); - } - - function getEndTime() public view returns(uint256){ - uint256 today = (block.timestamp / 1 days) * 1 days; - uint256 daysPassed = today - LAUNCH_DAY; - if(daysPassed > TOTAL_DAYS) daysPassed = TOTAL_DAYS; - return today + TOTAL_DAYS - daysPassed + daysPassed * PENALTY_PER_DAY / 1e18; - } +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 = 60 days; + uint256 public constant PENALTY_PER_DAY_BP = 25e16; // 2.5% expressed as 0.25 * 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(), 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 endTime() external view returns (uint256) { + return _endTime(); + } + + function _endTime() internal view returns (uint256) { + uint256 today = (block.timestamp / 1 days) * 1 days; + uint256 daysElapsed = today - launchDay; + + if (daysElapsed > VESTING_DURATION) daysElapsed = VESTING_DURATION; + + // Penalty scales linearly: for each day, add PENALTY_PER_DAY_BP bp (1e18 = 100%) + uint256 penalty = (daysElapsed * PENALTY_PER_DAY_BP) / 1e18; + + return today + VESTING_DURATION - daysElapsed + penalty; + } } From 0f1b7aa9cd8daedf6230c352b99d5c81553f14f7 Mon Sep 17 00:00:00 2001 From: ZigBalthazar Date: Tue, 6 May 2025 15:24:19 +0330 Subject: [PATCH 17/48] adapt test and deploy task --- .env.example | 3 +- hardhat.config.ts | 4 +- tasks/index.ts | 1 - tasks/symmVesting.ts | 2 +- tasks/symmVestingPlanInitializer.ts | 2 +- tasks/symmVestingRequester.ts | 0 tests/Initialize.fixture.ts | 21 +- tests/main.ts | 34 +- tests/symmVestingPlanInitializer.behavior.ts | 310 ++++++++----------- 9 files changed, 166 insertions(+), 211 deletions(-) delete mode 100644 tasks/symmVestingRequester.ts 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/hardhat.config.ts b/hardhat.config.ts index 104e1fa..b131cfa 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -44,8 +44,8 @@ export const config: HardhatUserConfig = { networks: { hardhat: { forking: { - url: "", - blockNumber: 26800831, + url: "https://base.drpc.org", + blockNumber: 29871098, }, }, ethereum: { diff --git a/tasks/index.ts b/tasks/index.ts index df2c488..ca9facb 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -2,5 +2,4 @@ import "./symmVestingPlanInitializer" import "./symmioToken" import "./symmStaking" import "./symmAllocationClaimer" -import "./symmVestingRequester" import "./symmVesting" \ No newline at end of file diff --git a/tasks/symmVesting.ts b/tasks/symmVesting.ts index 4f71fd7..836b2ba 100644 --- a/tasks/symmVesting.ts +++ b/tasks/symmVesting.ts @@ -42,7 +42,7 @@ task("deploy:vesting", "Deploys the SymmVesting logic and proxy using CREATE2") if (!dryRun) { // 6. Deploy the library via the factory using CREATE2 console.log("Deploying library via CREATE2...") - const libTx = await create2Factory.deploy(libBytecode, librarySalt) + const libTx = await create2Factory.deploy(libBytecode, librarySalt) await libTx.wait() console.log("Library deployed at:", predictedLibAddress) } diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts index 9ba6934..f764764 100644 --- a/tasks/symmVestingPlanInitializer.ts +++ b/tasks/symmVestingPlanInitializer.ts @@ -36,7 +36,7 @@ task("deploy:SymmVestingPlanInitializer", "Deploys the SymmVestingPlanInitialize const usersChunk = users.slice(i, i + chunkSize); const amountsChunk = amounts.slice(i, i + chunkSize); try { - await symmVestingPlanInitializer.connect(admin).setInitiatableVestingAmount(usersChunk, amountsChunk); + await symmVestingPlanInitializer.connect(admin).setPendingAmounts(usersChunk, amountsChunk); console.log(`${i}..${i + chunkSize}: OK`); } catch (error) { console.error(`Error in users=${usersChunk}, amounts=${amountsChunk}`, error); diff --git a/tasks/symmVestingRequester.ts b/tasks/symmVestingRequester.ts deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Initialize.fixture.ts b/tests/Initialize.fixture.ts index 6ff1a33..40e6ceb 100644 --- a/tests/Initialize.fixture.ts +++ b/tests/Initialize.fixture.ts @@ -1,10 +1,10 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" import { ethers, run } from "hardhat" import { e } from "../utils" -import { SymmAllocationClaimer, Symmio, Vesting, SymmStaking, SymmVestingRequester } from "../typechain-types"; -import * as Process from "process"; -import { time } from "@nomicfoundation/hardhat-network-helpers"; -import { floor } from "lodash"; +import { SymmAllocationClaimer, Symmio, Vesting, SymmStaking, SymmVestingPlanInitializer } from "../typechain-types" +import * as Process from "process" +import { time } from "@nomicfoundation/hardhat-network-helpers" +import { floor } from "lodash" export class RunContext { signers!: { @@ -20,7 +20,7 @@ export class RunContext { claimSymm!: SymmAllocationClaimer vesting!: Vesting symmStaking!: SymmStaking - symmVestingVlanInitializer!: SymmVestingRequester + symmVestingVlanInitializer!: SymmVestingPlanInitializer } export async function initializeFixture(): Promise { @@ -50,16 +50,19 @@ export async function initializeFixture(): Promise { mintFactor: "500000000000000000", //5e17 => %50 }) - context.vesting = await run("deploy:Vesting", { + context.vesting = await run("deploy:vesting", { admin: await context.signers.admin.getAddress(), - lockedClaimPenaltyReceiver: await context.signers.vestingPenaltyReceiver.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, - symmLp: Process.env.SYMM_LP + lp: Process.env.SYMM_LP, + factory: Process.env.FACTORY, + implsalt: "A", + proxysalt: "B", }) context.symmStaking = await run("deploy:SymmStaking", { @@ -72,7 +75,7 @@ export async function initializeFixture(): Promise { symmTokenAddress: await context.symmioToken.getAddress(), symmVestingAddress: await context.vesting.getAddress(), totalInitiatableSYMM: "10000000000000000000000000", //10Me18 - launchTimeStamp: String(floor(Date.now()/1000) + 7 * 24 * 60 * 60) + launchTimeStamp: String(floor(Date.now() / 1000) + 7 * 24 * 60 * 60), }) await context.symmioToken.grantRole(await context.symmioToken.MINTER_ROLE(), context.signers.admin) diff --git a/tests/main.ts b/tests/main.ts index 4373a36..b69dc5e 100644 --- a/tests/main.ts +++ b/tests/main.ts @@ -8,25 +8,25 @@ import { shouldBehaveLikeSymmVestingPlanInitializer} from "./symmVestingPlanInit 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() @@ -34,10 +34,10 @@ describe("Symmio Token", () => { }) // } 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 index 0a65f2d..b293538 100644 --- a/tests/symmVestingPlanInitializer.behavior.ts +++ b/tests/symmVestingPlanInitializer.behavior.ts @@ -1,260 +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; +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.LAUNCH_DAY(); - }); + context = await loadFixture(initializeFixture) + vestingPlanInitializer = context.symmVestingVlanInitializer + ;({ admin, user1, user2 } = context.signers) + launchTime = await vestingPlanInitializer.launchDay() + }) /* ---------------------------------------------------------------------- */ - /* setInitiatableVestingAmount() tests */ + /* setPendingAmounts() tests */ /* ---------------------------------------------------------------------- */ - describe("setInitiatableVestingAmount", () => { + describe("setPendingAmounts", () => { it("should revert on mismatched array lengths", async () => { - await expect( - vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([user1.address], [100, 200]) - ).to.be.revertedWithCustomError(vestingPlanInitializer, "MismatchedArrays"); - }); + 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) - .setInitiatableVestingAmount([user1.address], [1000]) - ).to.be.reverted; - }); + await expect(vestingPlanInitializer.connect(user1).setPendingAmounts([user1.address], [1000])).to.be.reverted + }) it("should register user allocations correctly", async () => { - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([ - user1.address, - user2.address, - ], [ - 1000, - 2000, - ]); - - expect(await vestingPlanInitializer.initiatableAmount(user1.address)).to.equal(1000); - expect(await vestingPlanInitializer.initiatableAmount(user2.address)).to.equal(2000); - expect(await vestingPlanInitializer.initiatableAmountsSum()).to.equal(3000); - - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([ - user1.address, - user2.address, - ], [ - 150, - 430, - ]); - - - expect(await vestingPlanInitializer.initiatableAmount(user1.address)).to.equal(150); - expect(await vestingPlanInitializer.initiatableAmount(user2.address)).to.equal(430); - expect(await vestingPlanInitializer.initiatableAmountsSum()).to.equal(430+150); - - }); + 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) - .setInitiatableVestingAmount([user1.address], [overCap]) - ).to.be.revertedWithCustomError(vestingPlanInitializer, "exceededMaxSymmAmount"); - }); - }); + const overCap = ethers.parseEther("10000001") + await expect(vestingPlanInitializer.connect(admin).setPendingAmounts([user1.address], [overCap])).to.be.revertedWithCustomError( + vestingPlanInitializer, + "exceededMaxSymmAmount", + ) + }) + }) /* ---------------------------------------------------------------------- */ - /* initiateVestingPlan() tests */ + /* startVesting() tests */ /* ---------------------------------------------------------------------- */ - describe("initiateVestingPlan", () => { + describe("startVesting", () => { beforeEach(async () => { - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([user1], [1000]); - }); + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) + }) it("should revert if the caller has no initiatable amount", async () => { - await expect( - vestingPlanInitializer.connect(user2).initiateVestingPlan() - ).to.be.revertedWithCustomError(vestingPlanInitializer, "ZeroAmount"); - }); + 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).initiateVestingPlan() - ).to.be.revertedWithCustomError(vestingPlanInitializer, 'EnforcedPause'); - }); + 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).initiateVestingPlan() - ).to.be.reverted; - }); + 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 time.increaseTo(launchTime) - await expect( - vestingPlanInitializer.connect(user1).initiateVestingPlan() - ).to.not.be.reverted; + await expect(vestingPlanInitializer.connect(user1).startVesting()).to.not.be.reverted - expect(await vestingPlanInitializer.initiatableAmount(user1.address)).to.equal(0); - expect(await vestingPlanInitializer.userVestedAmount(user1.address)).to.equal(1000); - }); - }); + 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 time.increaseTo(launchTime) - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([user1], [1000]); - await vestingPlanInitializer.connect(admin).pause(); + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) + await vestingPlanInitializer.connect(admin).pause() - await expect( - vestingPlanInitializer.connect(user1).initiateVestingPlan() - ).to.be.revertedWithCustomError(vestingPlanInitializer, 'EnforcedPause'); + await expect(vestingPlanInitializer.connect(user1).startVesting()).to.be.revertedWithCustomError(vestingPlanInitializer, "EnforcedPause") - await vestingPlanInitializer.connect(admin).unpause(); + await vestingPlanInitializer.connect(admin).unpause() - await expect( - vestingPlanInitializer.connect(user1).initiateVestingPlan() - ).to.not.be.reverted; - }); + 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") + 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", + ) }) - }); + }) /* ---------------------------------------------------------------------- */ - /* getEndTime() */ + /* endTime() */ /* ---------------------------------------------------------------------- */ - describe("getEndTime()", () => { + describe("endTime()", () => { it("should extend linearly with penalty as days pass", async () => { - await time.increaseTo(launchTime); - const before = await vestingPlanInitializer.getEndTime(); + 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.getEndTime(); - expect((Number(after)-Number(before))/60/60/24).to.equal(2.5); //after - before = (launch + 10 + 172.5) - (launch + 180) = 2.5 - }); + 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.getEndTime() + 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); + beforeEach(async () => { + await time.increaseTo(launchTime) }) - it("should calculate initiatableAmountsSum and userVestedAmount correctly while admin decreases initiatable amount", async () => { - const beforeInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + it("should calculate pendingTotal and vestedAmount correctly while admin decreases initiatable amount", async () => { + const beforeInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() expect(beforeInitiatableAmountSum).to.equal(0) - const userVestedAmount = await vestingPlanInitializer.userVestedAmount(user1) - expect(userVestedAmount).to.equal(0) + const vestedAmount = await vestingPlanInitializer.vestedAmount(user1) + expect(vestedAmount).to.equal(0) - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([user1], [1000]); + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) - const firstInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + const firstInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() expect(firstInitiatableAmountSum).to.equal(1000) - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([user1], [250]); + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [250]) - const secondInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + const secondInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() expect(secondInitiatableAmountSum).to.equal(250) - await vestingPlanInitializer.connect(user1).initiateVestingPlan(); - - expect(await vestingPlanInitializer.initiatableAmount(user1)).to.equal(0) - expect(await vestingPlanInitializer.userVestedAmount(user1)).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 initiatableAmountsSum and userVestedAmount correctly while admin increases initiatable amount", async () => { - const beforeInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + it("should calculate pendingTotal and vestedAmount correctly while admin increases initiatable amount", async () => { + const beforeInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() expect(beforeInitiatableAmountSum).to.equal(0) - const userVestedAmount = await vestingPlanInitializer.userVestedAmount(user1) - expect(userVestedAmount).to.equal(0) + const vestedAmount = await vestingPlanInitializer.vestedAmount(user1) + expect(vestedAmount).to.equal(0) - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([user1], [1000]); + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [1000]) - const firstInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + const firstInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() expect(firstInitiatableAmountSum).to.equal(1000) + await vestingPlanInitializer.connect(admin).setPendingAmounts([user1], [2500]) - await vestingPlanInitializer - .connect(admin) - .setInitiatableVestingAmount([user1], [2500]); - - const secondInitiatableAmountSum = await vestingPlanInitializer.initiatableAmountsSum(); + const secondInitiatableAmountSum = await vestingPlanInitializer.pendingTotal() expect(secondInitiatableAmountSum).to.equal(2500) - await vestingPlanInitializer.connect(user1).initiateVestingPlan(); + await vestingPlanInitializer.connect(user1).startVesting() - expect(await vestingPlanInitializer.initiatableAmount(user1)).to.equal(0) - expect(await vestingPlanInitializer.userVestedAmount(user1)).to.equal(2500) + 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(); + 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.TOTAL_DAYS(); - expect(totalDays).to.equal(180*24*60*60) + const totalDays = await vestingPlanInitializer.VESTING_DURATION() + expect(totalDays).to.equal(60 * 24 * 60 * 60) // 60 days }) - - }); + }) } From e85f301123860e0ce227145f901ea8b35cf4c7ce Mon Sep 17 00:00:00 2001 From: mazyar Date: Thu, 8 May 2025 09:34:59 +0330 Subject: [PATCH 18/48] Add input to endTime --- .../vesting/SymmVestingPlanInitializer.sol | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/contracts/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol index 372f3aa..77ca214 100644 --- a/contracts/vesting/SymmVestingPlanInitializer.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -103,7 +103,7 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable { users[0] = msg.sender; amounts[0] = amount; - IVesting(vesting).setupVestingPlans(SYMM, block.timestamp, _endTime(), users, amounts); + IVesting(vesting).setupVestingPlans(SYMM, block.timestamp, _endTime(block.timestamp), users, amounts); vestedAmount[msg.sender] += amount; pendingAmount[msg.sender] = 0; @@ -129,19 +129,23 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable { * @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(); + function endTime(uint256 currentTime) external view returns (uint256) { + return _endTime(currentTime); } - function _endTime() internal view returns (uint256) { - uint256 today = (block.timestamp / 1 days) * 1 days; + function _endTime(uint256 currentTime) internal view returns (uint256) { + uint256 today = (currentTime / 1 days) * 1 days; uint256 daysElapsed = today - launchDay; - if (daysElapsed > VESTING_DURATION) daysElapsed = VESTING_DURATION; - // Penalty scales linearly: for each day, add PENALTY_PER_DAY_BP bp (1e18 = 100%) uint256 penalty = (daysElapsed * PENALTY_PER_DAY_BP) / 1e18; - - return today + VESTING_DURATION - daysElapsed + penalty; + uint256 endTime = VESTING_DURATION + launchDay + penalty; + if(endTime < currentTime) { + endTime = currentTime + 1 days; + } + else if (endTime - currentTime < 1 days) { + endTime = currentTime + 1 days; + } + return endTime; } } From e7fc4440dad5e6cbd02841b8a9915ffc3566742e Mon Sep 17 00:00:00 2001 From: mazyar Date: Thu, 8 May 2025 09:49:37 +0330 Subject: [PATCH 19/48] Add seperate endTimeStartsAt() --- .../vesting/SymmVestingPlanInitializer.sol | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/contracts/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol index 77ca214..fe8b946 100644 --- a/contracts/vesting/SymmVestingPlanInitializer.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -126,26 +126,31 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable { // ============================================================= /** - * @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(uint256 currentTime) external view returns (uint256) { - return _endTime(currentTime); + * @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 currentTime) internal view returns (uint256) { - uint256 today = (currentTime / 1 days) * 1 days; + 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 endTime = VESTING_DURATION + launchDay + penalty; - if(endTime < currentTime) { - endTime = currentTime + 1 days; - } - else if (endTime - currentTime < 1 days) { - endTime = currentTime + 1 days; - } return endTime; } } From f4ee5d61b4861ee5b773f472aa7cf826001daf2a Mon Sep 17 00:00:00 2001 From: timaster Date: Thu, 8 May 2025 11:52:54 +0330 Subject: [PATCH 20/48] Add SymmVestingPlanInitializerSetup --- tasks/index.ts | 3 +- tasks/symmVestingPlanInitializerSetup.ts | 47 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tasks/symmVestingPlanInitializerSetup.ts diff --git a/tasks/index.ts b/tasks/index.ts index ca9facb..5f0d94d 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -2,4 +2,5 @@ import "./symmVestingPlanInitializer" import "./symmioToken" import "./symmStaking" import "./symmAllocationClaimer" -import "./symmVesting" \ No newline at end of file +import "./symmVesting" +import "./symmVestingPlanInitializerSetup" \ No newline at end of file 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; + }, + ); From a3dffde53fea052ba4aa61786bc88701589d76da Mon Sep 17 00:00:00 2001 From: timaster Date: Thu, 8 May 2025 11:53:53 +0330 Subject: [PATCH 21/48] Update user_available_symm.json --- user_available_symm.json | 6266 +++++++++++++++++++------------------- 1 file changed, 3133 insertions(+), 3133 deletions(-) diff --git a/user_available_symm.json b/user_available_symm.json index 8e1b714..9c86a6e 100644 --- a/user_available_symm.json +++ b/user_available_symm.json @@ -1,3200 +1,3200 @@ { "Users": [ - "0xAf372B93a02C54E231A9147869EF11F21E0FE82A", - "0x37633c9F778c44e852914c7056cBBBF75323Dfd6", - "0x24438f748083CB8a5f967d6B726C9aEeDF78ABa3", - "0x408A64365229C6097DAB7f23C8e2bBC906951567", - "0x4f8Efb4CC83a83e908029dB0a4c782B651a3BB83", - "0x69c509e9765c49CC6B9B4568b90Aa47B89F4992f", - "0x51f059C07E03d330B007B48630a857E582BE8236", - "0xBcbfD32b980E95d765C49728960DA9f4504947d7", - "0x7A7528C26CaDf1c4Cb3014f1A99f5F3204A77333", - "0x9633BAc0ae8d720E2016D8268a9030A6aCadD784", - "0x5942A257Bd448670Ae4353b06bAcDAbAb5FBF0c8", - "0x534b86280545b270b0B0fC2A57096C42290b801F", - "0x60A2E0ddAC932aC61E38B9cA80aa75926966e01D", - "0x941689a3677aE5D7547aA6b298773db1a50E2Ebc", - "0xB5f69d0cBD702D713767C5442122c156bD6C971c", - "0xB5e0C5A26Ed9cD9c1684Fbb1138D5952C53349F8", - "0x6573948AEf66B3EB45f35AB5C7516678F86d7CC2", - "0xA0a3eae0F173567B9ea2abD8b53099AE09d71114", - "0x34457cA3A154415eE1847d311810B33815Dc7497", - "0xCaF76ca5cF79510F7928EEE79EfEB92372957c10", - "0x6ffbb6C376d0cf590289671D652Ae92213614828", - "0xe0ad5F6422687bf667D09590383FdC3C0f8aF392", - "0xECCb9B9C6fb7590a4d0588953B3170A1a84E3341", - "0x8a6a3Fe565451f72876d88Ba55C6152bBb0c1836", - "0x062B0613A91603975B330C4Fa4bbd17feeEfD130", - "0x2d13CD9A79e68974993D19451fe615925557FE28", - "0x7A505dF95aAc78f142CA98CeCd771d3c398b4b71", - "0xa973d0968d6BbE4859baD50379d87c91Be615B9C", - "0xaF1ecb33e5B49F74c020C0E5285d73504B01E051", - "0xAd52E66a1ad6Efe114Cb9278dbF9338e37A517e5", - "0x4f4b9492392b88a1D21611575B732428a1BeDEA6", - "0x225948313D4CC7B5953Ad698740eC333Df52a8f4", - "0x1255E17ffF07322231A4124Bb455C85d20d0a2c4", - "0xe175c44341736349fcB4c1498eB27d77b96d7Bb3", - "0x98ed1eAE1836Fa93A20FA389Fc386bA75bd07D5B", - "0xA8c24C16ad11D008eba1695D37417b47aB461a98", - "0x69f8D754C5f4F73aad00f3C22EaFB77Aa57Ff1BC", - "0x6187053cb6Ee01e5d834A0A039dA91060Cac1Cb0", - "0xf43257DF6B2066C866ceee61218ee1Ee50D3be6f", - "0xDAADaf4F2D94cdd4274eecF794C3d5c27F647476", - "0x44f4DA18D1e9609E13B3d10cD091e3836C69Bff2", - "0xDee7e15ff122C733c1f5E8800187815391c8bBc7", - "0x848dabf4F7097617AA482110615E5E84e4D5DCDD", - "0xACF902451683Bbc6f1bd5727a8e7E6B03e5D08e9", - "0xA02D5126317c5cE85ACd8B0ad1d35f944Beb441a", - "0xE72C1ADaBEbA9F743Af60D4dEb45bcFa090a5320", - "0x679bCDAe56EDf5c847e769Ca9eB82Ab58C93351a", - "0x65B064726FFfff3b3667718d2264E84367Be5141", - "0xa219712cC2Aaa5AA98CcF2A7ba055231F1752323", - "0xa25e3A5F8268538c3726CcEc9624056973b71d2F", - "0x818EAC5be158d2629702051E4B8c24Cb27829AB4", - "0xE9E2C06423d41e91e63DE4Ab4A68b3B4db0e681a", - "0x7ADFCCFd3691A99530F632EEc3dC480419ADdd7d", - "0xA9433Fe051aeE57B1DE0283504bE74B1D6cD39ab", - "0xd8fA3942e3CE77B6f98Fb17B2564d273C0b67d1b", - "0x8922923059388a4488Fa3b7C418BD059723bD37F", - "0x021F1c3677c19B13093eB39C445A4847500260E7", - "0x29F82d09C2AfD12f3c10ee49CD713331f4A7228E", - "0x2CD3a242D493Bb8aE3369d3EE2b83B5bf4fe82D7", - "0x4acCC4f0781594dEB1C382EA8a04B566927CfbED", - "0x7B3fC9597f146F0A80FC26dB0DdF62C04ea89740", - "0x79BE4bAe05714B8F50C840A2B4bd4D8B52f4233F", - "0x003cdd05da057a884803D1ba3f5De8aFdAcf73Ea", - "0xB444776908Ff797E69D516caa5CC22856781B0A9", - "0x2B2843A5CBCa8ece395665F3c545707Ad4b3d155", - "0xd2cbB2EfC68c8F7819807EAe033CD5c84Af8e235", - "0x06ac42915daFd5166505377713b849Ad85b56a87", - "0x95DB2fEf9F52Ba3F8CF4548374b9135dEE83E8c5", - "0x629f6a7335531791eBeD6E7651634285c3280334", - "0x423d1542697d3b737151c1E7fFeC5C82dd5b4B12", - "0xF2978B5dABb93964410FB0394c07f144706a2DF7", - "0xb2Ae7773e14d857D1202cfc2A6A5a2A3de1525F5", - "0x6903E7e1B1d0b67a6Cddedd59d4402eEe2be3797", - "0x5275CC5a4Bb7Fa74f602F7Ed90b1Bc3579674FF9", - "0xDf46bfA73936458e4BC90E3d1A1CB60Af7e55672", - "0x12fDD8f4EdD6dF194B0b974F21B1195Ceee074FD", - "0x219EE0C53D8d8CdA8700647b1e2a58896a4Dc5f1", - "0x82D1883ca96e57773429E785F195E32783B1C246", - "0x9CE6E6B60C894d1DF9BC3D9D6cC969b79FB176B7", - "0x836e71f16A53577eDd2Ea377A8aB29A90ae0cDc0", - "0xb305466DFf982283dB8886d8b2B54adf88A1c467", - "0x3B1fc9653f03789BC399fC343d8D5B8fC1520EA0", - "0xd3d5458C07B655Cd79d4814A52f42EB8Fc59c24c", - "0x6a5334E6c9352C1b5735969DD439e41BCF9b4dA4", - "0x623331FD48abca0AF5457BA39b8328979C7FABd0", - "0x38476bc1deb12F317cabC3F49fBBb80FbaB6f3ec", - "0x12e610AE4C0871444C0539fc7563C5D3317C615c", - "0x76c2CD1b8A249F465F8445c455CB78E0eeD36f1E", - "0x7A9664538A303dB562668BA8458ef283f4323019", - "0x9e17eD28b590d0b422552b961AA685Ce230aE931", - "0x03E236ad091A944b04e0311d329161C2A15d6012", - "0x14da174FaB1356466aF2dD0ecdF94204E828A732", - "0x69FF5358fAe4bcc2ec6db6CdB30D43d129291709", - "0x5e53Ce97fCd3AbE46F1aB45abB3e9A9C5e19F193", - "0x304B3D778ADa6D14387dba400d48eC5BB9EB689b", - "0xF91B7e024b16468f45f53278609f1B13eBbD5306", - "0xc0A674041C3CcB2DD8E056567Bcd6a14314b63c4", - "0x2f374d4Cb2D5aA194c8279D6607D5EBD59493C3a", - "0x8577F44aB55c0b1167031eD26C20a4DbE26868dd", - "0xAb99C070540CE0aa4C7B978584A6edFcA5af37CE", - "0xF75db6544CcfC7fb5A9dF01B9A3Eed050562d0d9", - "0xe2D6AA050C655a3F0f95eFD8F2107778090308aE", - "0x8056dc8F2Fc8962A2A639651b3a969bB6223010d", - "0xbE782eC98CE7135A056E31300a9831fA35D52865", - "0xA7cFcE25AA5D9752C8ef5a86D5D76fFD7FAF16F3", - "0x896d723154eca634E5DD8A5D3EE76A4F1E9bE22E", - "0x7d0A940d81cE63cE716a1e3CD7372F78F79d2414", - "0x25E7Fc427E81E2819FA9c455131c1f857e42A81f", - "0xA2863FCCDD9Ef5D378374eA6A6DB5708654f0056", - "0xe9f6B6Bb8cA582e0E5DcBB03E95b247843fDCF2F", - "0xE126d84a1c2018b5F5ff821C1539eB8c53F27bF2", - "0x1B47235150bCCcD6B51F60F175174dD2d579Bc13", - "0x01D05B80aDf3a8D855a2560a7d89EC32A9F037BC", - "0x49FC60bC9c4420c8af344966E50b6f1B4786910a", - "0x072e333E4C5B434A59e74f8876035CF81E5f248e", - "0x8BbeCD49c2b44c2eb8A27E8a42f49B09cc9f0F31", - "0x36C4C53bB6b0B8C59E98b35a1EABb5d43B5bC27F", - "0xe2Ff239cbec4db2bb8273b4e7c4F1E531794d2b8", - "0x332df51FA1D8A2e83BCbd1069b771D9B632506E8", - "0x0aa14f7De21E9c8EE836D2aa73aeC761B34b18A7", - "0x414D5DE6fAF79b80069212efa523C9F8e825702F", - "0x18A428CE0c31584a21CF8ceFDb8849d8013E1994", - "0xA5504BCB6cC0492344151EBee943C62d2B2c071f", - "0xFf9E5E8cB270d80c2Eb6059c6B0D70a9aec96922", - "0xAcb17fC73Cf202EBe532E107cEc195d50Dee61DD", - "0x9800F669F13c4957C4e11f9C4Ee2678DEdF7929b", - "0xD6AB3210442765D0BD806Db6643Da28d307eb422", - "0xe1ce761AcA1cf320D3185b6eB37D58A81FcDaE2a", - "0x64368FAce21029A646d823c0AFcBB4dA08Ce9b78", - "0xeFF753B70b34ea0DB977ecd27C670Bf3FDa68EED", - "0xa0E730ae29d4DdbC39C40F94d799b75C3E17Bd58", - "0x54f18b69698D9310E8c645d2b498715907a6ec6E", - "0x043f1d06806Bb1de6aCAFf98B078dA1a6Aea55E5", - "0x1A6e210DCdEF318CA8c11AaaAb7e305E49Eb0e7D", - "0xd1779622B76824F952bF89b3bc009F6dB19BE701", - "0x2D94Dc4D69DB8eCDA720406C118702c0719b4345", - "0xF0FcB5B2eC2D511C4338F59528BaAFDaa1f3ff37", - "0x56c1589B0b6899B9D6e9334e98C9d16d8082cd91", - "0xbEC85812e620b56525681312B12eFCe711A58135", - "0xE49f9C282fE9C9BEc52905Cf11fBB93a59568Fd9", - "0xb9d1A4528EcE05D7451A1E8f42F60c93a390e350", - "0xe0Ba6c4c82376c76386D1Ca2ae2516Ab18882e3C", - "0xbFdd62139F97163D83Cf5CB7F940564b56c6fA5D", - "0x3523481702186C04740e50396D0e1C807cE36C57", - "0x25DA992fd0f68296a2659B69f1642F0d55360993", - "0x3587B15f7865D4F3F5cA15D29d197bB2f1E6309d", - "0x8a70b9FeFd03D8C54877bc26203A87f253266FC8", - "0x592e1E16016C25651513591B7eBed5ca803ad39e", - "0xCd710d5Bfb7A7e85dd19fcf0470eb24Ffa057858", - "0x0fDf1Ec1F12dbbF56A8b066985B1349cbfec262c", - "0x0507B3ffe3836aD0f256f3D53b8fa7653e54E2EB", - "0x3d7f42a6bc03Ed9dd42b9853EA027EB5c844A36b", - "0x7274a097d392a564622fA8569599D04fF30825db", - "0xa84842F23737367274B6949e61f0bA8f3239b0Bf", - "0x0428fdC1f59747b811Eb3834344e065c216241C8", - "0x3Dc56e85283cA780d5FBda7337B6BC98a5E4f22f", - "0xD126C57B7D3D03F2280dB685664E9dDc30300146", - "0x119030aD7E6FEa8f694782898038da2E768cF6BF", - "0x99e4b3a0217f7e45815CC7726CaEeC63689a016F", - "0x875733f4C30BFF1fa1ECC78045CEE4B8a15FF08E", - "0xA6554D85b945Ce01505005bb07e56e6126259686", - "0xa9E839484195Fd7685374f29D67f68590c8B2bDc", - "0x340209C8975508A8B6750C01Ae290Db038a275c5", - "0x9f72b48ec2410586a49c75963F094B23738a0CF4", - "0xADDADf3Df10bebFFf7201427d50Cc0448e2E6F3a", - "0x8f2b60Bb82E11fA9539bbf88803EC079d25f1B86", - "0x096eBBd5FdC3B1028Ebf2279820028c0272412dE", - "0xa3acCb1FAcB57a66720B1873466855337B6d5A20", - "0x0d3409eEd02927d6298ab98BC79496CbDdecf0B7", - "0xA6A2D935363669EA08723798284bc3Fcdc58f9Cd", - "0x6A16Ba7B1653d19802d40b45BA4aDb74170770C1", - "0xC8A7403AC82432c8eE1f5eF998874ff3d185573B", - "0xc57684e4bFD9fd5227Dc1C66BA03CB9592f4e492", - "0x3e6EdBa22D9cE22a7168406d26598AE20FE08e41", - "0x86Ae657b0b38429E8072523F459e572F777cA276", - "0xCd93Fc667df20D3ed9B0b80f23a1BF415A9085fd", - "0xfF9c6e3aCc07792649644E97e358EC60Ad22b018", - "0xd699571A57D3Efe7c50369Fb5350448FA1ad246E", - "0x07Eac6Dc2AF876d2F0402f6804cc6cEAa0cd20Cd", - "0x078b7b9520514eEc7e767574e5F0BF33C84d910a", - "0xbE026AE67432eFa9aD85b897A70e0BaB0Dcb1CEA", - "0x8E29D0E2CA8e92a9f27192616E2E9f170FD2A035", - "0xD522d275F7b178DCb949f96e5Dcd73f6995760e1", - "0x6f199F00655e0D3e9c1f2B654570758CA1e33759", - "0x465FE8f2122966769b3d9D1E7391Edf0ac08CB43", - "0xe9c2F269ac049c6D2C3f893EB5465F8B33E561FC", - "0x5fD3140Cbe45241b8eD6e7c4648c2AA8068791F1", - "0xA0b427aAF79BAc6031A2cD2b7BaBE1D4f827aA04", - "0xcc731095cD449EABe6b02a313d78Fd65F266CC29", - "0x6F85f16B35a6BCDe77Ff085634875CA608c826B4", - "0xc07686AF7aEe9353D8E88febfbD72ec68411ccCd", - "0xE43fA6CBb7DdA0bD863593c5E75fAe9B4dCD5CD4", - "0x29d112c777Fa76d1c404502f0e3Fdd7B7662e1aA", - "0xdBF1d832d1E66Ce1D8498D578364C0D7D0614C7B", - "0x9550bF7dF4065A0a0d57340b603478cAD4c5d465", - "0x08d97F4c2f0225058D096F7f6B637007EFeE2880", - "0xc87C7A63FcE82e5929524EEbFf820A16FDcA1147", - "0x96501927163C32ba990d0Cbe6f42593c88460Af5", - "0xDCB25ef40Ad51bEBc2e63D34E5A15E21f85D212f", - "0xF93ea91d48E3fCb4A9D710c6493b4F6BCcBE4943", - "0xcBd9f0c0639FC515ef64835354935Ef14FBF48a4", - "0x92656aD77da2e82731d3cc083116960Ce29DfD74", - "0x1338FeF4210fc6095226fb6388077c66f153087c", - "0xcac88c05AcA7456FAcA8a4f0a472ED00a2136D9e", - "0x7D9F51490414a144a143F31fEdc434Fd96052AF9", - "0x527465920dE22AD83bAE0043f0bf975D52C9C63c", - "0x44bD6FD314dd5AaE5100f7A9932575207fbc3bd2", - "0xF712aB8c4505eB4B36a2Ff9Fa746470be81E6538", - "0xe4b7FBC0c55299Db43553908C36C98AFd641D3cF", - "0xbDec7287C08C0c153F9843C4796D59e3BF7178E4", - "0x60Ac0b2f9760b24CcD0C6b03d2b9f2E19c283FF9", - "0xB5d64a8720E8dd600AA30965cFA14799DF6824f5", - "0x32066C25EcE53F6844b35C8Ff155DE36b40550D2", - "0xbCa701b4FfA41a73B483ebd1072E8C75BD4661eD", - "0x27F0D8Cced5F180a330EAD4Cd11D4aCB1e42FCea", - "0x28B6182569e04234Ef77bA48869dea352933d168", - "0x61545187D0d2DCeec9261f9c358CAB14AC8A4C0f", - "0x19246d4cB92671D0F2166D00C9de687Ead3206F8", - "0x00aB97c782c661fb3D08C96dce06176938101211", - "0x3096D872E1FCc96e5E55F43411971d49bB137B9B", - "0x4291ab8a26EFdf45a89F1b4bA2aaD8229A778a9b", - "0xf291280602A6Dbbdbb08D48557c41Ab4C45F8b49", - "0xD59586b52f6F8c90C04c2B9718bE7b2ac7D6Cc61", - "0x9acE991de0bD8F2E32a357024df85B40283319f2", - "0x96A6f29ca861B7420c586E070Fbc767E357BA821", - "0x75AEC1D6Bdb2d63BeCFd6c65A01f3E2175B98A62", - "0xD7620731C75B0F4078707be8F1Fd6B6b7b277A3E", - "0xC817EFf12fD883b20b9Fd91dF00B029390EC1FFB", - "0x33d835ba506684273A1d1D07F2F64d63de389150", - "0x6b6cA48DA27304eeEb1D15d355D948f68b852733", - "0xF0DE256d741910BBe86173f02c795E1034CdA0DB", - "0xacFb5D441F92339218Ce5B34A794278001dE6073", - "0x15Df788d6a0253B91cc2694ee6665420247E381E", - "0xbD11f25ed4407b289bDD27D0668435A3493a4E02", - "0x30F89f5f4cF25f457B4D9cBFb64a64A97F00bED0", - "0x28DA3DdE285D8F1f87B2D858f89961Bb8B9Af180", - "0x9695b507A59f00f2fd667525D9Fa661Ef0280308", - "0x9a499155E6D5F6A8722876a5AD0c4A47b3322C87", - "0xec04e0a492f5A5fe631AbB4c7dddAE8A2c84e57E", - "0x765E9AF7210c2d120096cE5b5709e8060B710474", - "0x5DA318b6F25497bEeb5FF3783582A01D27623CCC", - "0x0F45156F109e474295913D78036FA213b1745D5A", - "0xEB4576fE753DAB07635c0Bb6c8f0A355e1Db5d31", - "0xd210ae3Ea4bB169163F4e9876109A2C204Ad3a87", - "0xBB8aDE1A31A79422B425e876184724c209f78289", - "0xF6ccF8751fd02DE2e14cDf0Ff0Aa36D6e7A1aB28", - "0x3a28F433F23E17a6201552b5076869Eea077b6f5", - "0x3692854Ae1cE9a91Aa0Db6888105B466C767e112", - "0xe11925DdDdEd86a78552ddF7100b451CF22743b4", - "0xe601043c33d78EAEBc8e27e0436e7A0d11efe1B0", - "0x24046EC223b7a42EABb0527D60E0138922bb933D", - "0x9C94287160B7FCD729aD6c582440052a949Ee96A", - "0x353D566AF2B571F6Ade5Cc9F7a91422f6f738098", - "0xd9a62b4938ccec820445009Ab91b71cd1fC6CFF2", - "0xbefF46F500c3Dd2A6301b9D869fC49B47302327c", - "0x9a483103327b3b0814ABCBf61C4860C48fd84f07", - "0x36D6c29e957FA8E533F1096711CC8D70931F101f", - "0x6512A1C7D144aCa84fF06dF1e70b84Cd8186a949", - "0xbEda49BdBDb8aBDD6993B8059A8192147FBB55aA", - "0xe0A86AFB73c0922A50D4A985A25507EcDA8a9B51", - "0x2E792c5e7827e8b6BB26761E2C4c44B867838166", - "0xB44647814dB8aDa1b67E8240938d2A6bf77184Ba", - "0xB83eB831f49c4fC41Ae14851b16820a233eC0cEa", - "0x0ff51f912EEa28b6195C2db43e0e90Ba905B5b42", - "0xD996DdB8b48e3728206fBF0bfc91b4378d6C0C51", - "0xcF1Cf25e5CD7aEA5f86D8400d600411BE1d85D90", - "0x99AE3F791EB80db2a96a1885793c5004273cC7Af", - "0x73cCad884e29ef092a7E8B3e943b91Fb3881a7D9", - "0x006FE810cb3830872bcB217dFFf18Ebf1E33B290", - "0xE2976f612E6220643CF23f02735e7c7b946da315", - "0xed933b393Bcd6f138B3843f5bCD50E3ab6E673F8", - "0x173EDA92A4e3F1B8eDeEB02880787b36736eD937", - "0x5Fe3661Ad3Af1A2Aa85001364B988Cf628C38811", - "0x789498eE77215b80d201A814228fA7f248c3be18", - "0xBAE6379ACaEe0FD0d5922CABa1895292533B6FB9", - "0x6fb5421989807f828926b03E44A5Ce61D6906EE3", - "0x1e54DEb2D2176f715f719d328Ee35f06029236AD", - "0x459E634907E0722c7145EF56765FbCD924e86303", - "0xF0eeE1c6CD279Aa9F8c87e8BEfe0b688440db1c7", - "0x2EAa6Ac9dc11B1C4700443f52FBc9d067C1D0D65", - "0xeD39c5e2D4B2Fd31E0F9a62fa5416BAb8C062a3c", - "0xC766545EF68b49E5C70F4E04C52a9caf27b9A9A6", - "0xf2c9c2EbaEBfC38f76d825b09A91AEEC477F4fcF", - "0xCC2e601F4e686366B7a0a581EaE4D26b0768F4b2", - "0xcfAACE98e68CA2E796ce396760515606fc6855AE", - "0xc19eD1FA9FA3968FE12e97Aa4d511aB6bCE1320F", - "0xfc3267Cc1eAD34059f43A70Eda456b29D1D64214", - "0x9aaED9Fbf4dAB79296190817b85645D3c9DcbE7C", - "0x5b79264F6C65AA1f6B447AecDfFdBFeB8d71c18b", - "0xCF2C2fdc9A5A6fa2fc237DC3f5D14dd9b49F66A3", - "0x65877BE34c0c3C3A317d97028FD91bd261410026", - "0xAd24D3708240240D3F141580522B13F01e15AA9f", - "0xa380dacE2095Adc258d2bE066e2A60a9DBacF7aD", - "0xF2ba2EAD3D26C213721114a02E0C56D8631AE388", - "0x8Ff6FB658244BDF66c051fA54DB6768Aba552E87", - "0x330C17CCD25839DC9Aff158A30Ce42959D249EA6", - "0xFECEE8E7642A25E6A07E0F5eF4C09cd9557F6BB6", - "0xd6339759848E5CEf0a90fb6bc3406A62315739c6", - "0xEe700cfA2324CD9c06be498cc65DE24b6c3D2893", - "0x1aD4A9C4Ca9c0A6b1A066dB8447873120F2FF9dd", - "0x48BE343c86e5D2233e874B23Dbf123B0C2f80857", - "0xD65647E84C4eDc9A0c9e14470Fca64CD2Da8B35e", - "0x5309a0f7E9F92f535009bA68172b01806d223A43", - "0x9D1F79D20b9327B54096CA287f314a2e8B72Be36", - "0xC8Bf09987727D53DEffaf02B2BB1b223Ca014bf6", - "0xB050e0b02736873BDCB09F1b289aFA79744ef33b", - "0x54207Bdd0b6b1DAA5Bd98c0c8c2EBDEf65f63a3B", - "0x42A82dE905239E0c5e584e0C80d7a15825B9c190", - "0xF62bdAFc4cB3808346c5854c0Fd129F2a22c02E0", - "0x5fDdAF29681F3aCD3ee5D88Ef150F066b4db9890", - "0x73dd0F5b465b57570156F25907Fac0f38b05aD0b", - "0xA7Eef766B1f3F3d31A0c7C6Ebd0B3f31e9a87634", - "0x069e85D4F1010DD961897dC8C095FBB5FF297434", - "0x4A7E2122685eEa970ee0F5F257c62400d345cAfA", - "0xDD30dad7a3EfaC0C849e5c949dC1fe6B233a3FA2", - "0xbD65E3E2D82430Ff414685cd5b4DdaDFdC8b0686", - "0x0e90CaA1DDF2184bfDE42Cb7daEe3642F9fF54E4", - "0x0d7dc488F8276aC1e651D982ae6A16dFf089D813", - "0xF429fBF67A42576fA6d38C310072463e79e2936e", - "0x197B7DfeE0EDbDa94d8e718378dfe569E9C22d42", - "0xcFe5D190Bd42Fca25c675CD0910CAf1e985BAF93", - "0xBbFbFF2FEa1950d404742DeB24ECe126f3796506", - "0x71535AAe1B6C0c51Db317B54d5eEe72d1ab843c1", - "0x4413A906a012F06bD0ABDa5eeC45F7D67b449284", - "0xFC9e0e4F0359e0972C02b840B9c36E129f82ab25", - "0x4564d6d107a19d3AB3D734A7BaE61Eb63dC3d30f", - "0xE773558E6b7C2c27c35550b29D1d337151321986", - "0xcc112a107ABe98901B9EfD9ac4880A143dF48083", - "0xb3A81311c3A178928cf96F5092d627a259dc5aD5", - "0x4334703B0B74E2045926f82F4158A103fCE1Df4f", - "0xEe650F5599Bd48F27b29B254A4C45322A755C6b4", - "0xeD02383d3B21052A02EDa1b456f0F154738e7a2D", - "0x83cD4F9d40aF885DEc803e0cAD2156246110ABF7", - "0xf0665d14A7d67c7A08122aED9901cD45fE0D2b79", - "0xce3e142Bc70F5a0f674A2ef5649b3248842cf1A5", - "0x9125b2457479964540a0557e3B010681317B635E", - "0xaDc92E52414bB1A407f6216230bE1fF574f7042a", - "0x7a69D06fd825fA64ca81cf8dC9710Abd7cf05C24", - "0x63d293a7F2ac5443cad758FB481aB32442d4259d", - "0xDaa8cb6AB1C9AffCC012A5A023EF1B79f91c6297", - "0x4f1Df0B690c00bD96B1E71C66cb4E75cb22Ef1d1", - "0x984F47Fec079d3349bb3AbDf05e40720E1C2d968", - "0xB0720A40d6335dF0aC90fF9e4b755217632Ca78C", - "0xD18B8d61c160e779ceFEb61d47600061007a6130", - "0x4367Cc270D68925d9bA89CAe2A9c2877f3acD69C", - "0xdF3f35BEd840A5ffDCe0fA383217a19A7C34E0B4", - "0xDA7Ece710c262a825b4d255e50f139FA17Fd401F", - "0xf28cD05A4d9C5dAb90C99983c996343F0DD006e2", - "0x2ec9566FEDe31eF04D9A5d4fF9DBEAeab50C5273", - "0x68CDB7D616a0a2703044bdBC1fAc5E03167aE7b0", - "0x7c895f59b8eB7Fb88ce41bF03156650Ac40E16f6", - "0x0c6a0b7Dd1063ae7443AB562622bdB4603dddAec", - "0xFa4E885E98aEBF061bCC90F6C85880b45e5783ee", - "0x0403FfEd505E4fdF9bb13309BeC69EaA803b06e6", - "0x9aD08e2742696174e1277f7710568A2724eFa82d", - "0x2Fe95bA46D36e6Bbc8d3bbC6BF3F18c85C91f3aF", - "0x5796F85DDE9ABfC2bBB28d086DEE994D970a4561", - "0x259Af9A30cd456D0D614e60BEb54E15824A85a1B", - "0x1125917201ed36700F86c3CecEC8C5DAfAe280D1", - "0x56Ae42256F47d488F74efe18089ec1E83f0ab9d8", - "0x297b84479d7A6Fa254E13fF8aF9a39A7b5d4D629", - "0xc8f64d227c715aE078D642C0e3a15D44FE7a48B1", - "0x43DfcD47e7c5C0059840883D9ADDE4149C8C30FF", - "0x9eE6f42531aDaC0bD443756F0120c6aEed354115", - "0xe5Fae1A033AD8cb1355E8F19811380AfD15B8bBa", - "0x1406a69a6abD0A3D904876724b20454B3EcA5eeD", - "0xc2dc013edD48afDcd7b872D51d55ccd1A7717e28", - "0xA4A62f7127C644AAEf733029E0D052E411fdafb4", - "0xa7ef228999570C8E507E6ce7D7bdB12fe053b7a6", - "0x716410bFb864A494dF9b943a63E942E6E895d21c", - "0x825FDbceeE8856cb55EFd3b0128ABf386eCe2315", - "0x802cd7AB8E2c7a8e1906063a707f3FF8004AC748", - "0x38B0d0d924E0A060296CC8B39f436Ab6b92745F1", - "0xc655B790FF812109c8F6c3f24fd20b3495164A51", - "0xb02dc413e90867f035Fa6d29fb54A1E9aB7b274b", - "0x77eeaf07C050a690f9B3C2E8e7642Cc3CBEcEEb4", - "0x01650a012B91e32A077135Bf7C9881C44b9775a5", - "0x5D824ac4b69776f5F3C5961043D684FF6091fAa4", - "0xFB0873956e94433B4b4AD4f07d6c1fE638F870B9", - "0xD583F58E120600961e3e44dEF13718fCC1C1f491", - "0xc02cA57CE4Fd197Bd34877791be417B03Bccc049", - "0xf1Bfc5760C1Aeb6Bdb16252142429107D4Ae0997", - "0xC677E71B02adc94534De993A907352f748d21143", - "0xd511A8D9448Dc95e774000dcc342207144aA9377", - "0x9Cac725F17d5deB4a65922a6043eAc4081aC6Bc4", - "0x9400e3b00AD9a7BbedD6912cf92f8fE34adc00bB", - "0x104f3DC171f5CE1d1501A49E664bA1fc30252Dc5", - "0x5A924579Ee6d0b8F8172c944A5770625B11b635a", - "0x42e4D76f5b78eAb5fc07098FE4B5c35102b5f4a6", - "0x643e1004dCb9B37d00442438f69D306473fDf58D", - "0x8131f8bC771733D44500CC35464fb9d451932269", - "0x4355Ea5F88d29EDb19c8D5861b11CAB5C0F7CDB5", - "0xabC3B8A22B766b9C5Bbe73724D2F35f81359e72a", - "0xF28dCe94A86f64606bb80B38182C2962157BB2Ca", - "0xc3A4fB7Ee898f675bd691c05F43aD5A67aEFa81C", - "0x62058F928E0b8af1279670437d7dFD9E55e34560", - "0x798ed654F4D9599B99f94F4A61609b7A7cFBD200", - "0x610BB423d080Dbbb9eA5fb8BAb100AAd2D81BCA4", - "0xE60e0A75Afef85809763d703c958F101bF7B5C45", - "0x47C89b9a82a508cD5906D24d613EcC755cF4BD41", - "0x61d7ecE32854CaF46c6304Fd536704EacD4dA256", - "0x5A9F254F6464C6BD721c0847C69612B603f09673", - "0x4cDd6Ca7dA844AAA5bE3c7BD5b9CbD213c77F6c3", - "0x4BDFAE7d2A6FC206c5a80f4d41e683a5D5b30D5b", - "0x85eD40DbCa94B5BC73c6C7EC7f4eCaaD04A03a22", - "0x59dfe67119FE222F4a5485B15C6F0B4644e43E8e", - "0xA22fFcb5295C1bEB7238E3914E19e0bC18Db8f23", - "0xe766cA34C2600FB9db878d6C916e8A8B6f445e8F", - "0x5c533d8bF5f06a36fA8996C81fCCc30FBD04A9FC", - "0xd60a9bb677B43820D4D3E8C132d1EC60dD1d0313", - "0xacA3bE620ab0378F551531fCE89006A358b84EDA", - "0xA529DbF959fc80F9Db66d990b47Eb33e26c416a0", - "0xC8A44E444e7dAc7744bf2FA35256F7300e9c04FB", - "0xAdffC760EDF4f6146dce89022B5AE7EbB7edD2B9", - "0x708B1eBc569D0B4D127fFd2aC5554EFEC032f0aa", - "0xE476DEC76e2cc238633d1e361b5A7155F46099a3", - "0x2E15D7AA0650dE1009710FDd45C3468d75AE1392", - "0x9763bC90dd4E1544da011833aD1Dd25b8C0EeA96", - "0x63F19569f0f729F137e1042639Bf867eAF8E7b18", - "0xA6e14452Bac0d4CB25F788f1DE0AF7DECA87f514", - "0x92b39b14AA07C309317Ea0c1D5A76B2808Ef0ae4", - "0xbfFeC9dB5D96F5F8367c3c8E95C25eb8B7d44AD6", - "0x93e961b3A375C1831db9C3cE604d4b54F731B4d4", - "0x14fd7D24eBF81196dfd6d3AF740D5024071859f8", - "0xbe7DD82FACD47D3598803F44CB980078466aEBA3", - "0x17561E4A5E16E619e049E3A61A64F2A2564859f8", - "0x7f4437dD2881468525343b86ef4598d24a21bcAC", - "0xdA90a6e5b488a617C49a412A198b8187b7e9c999", - "0x7b83FEbc8cD693eeb15bED36223DE459B0ABae04", - "0x5eADDA0D4C686978982a633fb06910F5a2FccfbE", - "0x0e011CF6a91dC8C3CdF693b0F8f214760dB1Fc8A", - "0xd79C597a5d5e4b093c4DE8DcA8B9c7927d0d4B68", - "0x27107DD841eA4Fd7535C9f7905BE3a5d0CAD374D", - "0x841382062d00f024A7a4Fee09e42e08DF41dD714", - "0x48E87c81A1aeCe2a39B4FAcD8Efdc3FcD63c636c", - "0xD79B646031aE71f77DE7a50CC5F189f5c62b7A5d", - "0x989593A779906A51596e84665E24fe2ef4b1F3B0", - "0xFaa41900848b7f68F4859676432811E6fC46a15c", - "0x4c807964532Ff73684740000F891230417EFf375", - "0xed84906A819e3Fb51C0b62Bb7D7f86262888BfF1", - "0x714Be2AAfd387bF46cF5b9D48d596A2B851e5681", - "0x1D38DD297F253837eF2b763BA9fE2EeCe45c48e4", - "0x22f851bCF7353d072Ed5Cce3cAff096195D2794e", - "0xEbECc0a1eB0A05f3cE7100f20Ec6b97cb93F0965", - "0x843fb684D6767473965BeC92C211C6De690f4B27", - "0x5882aa5d97391Af0889dd4d16C3194e96A7Abe00", - "0x8f35Df47832dC2015011D0af1D5F1aBd6f7a905f", - "0xe1dA9E3ea9eFc074EbFfD4D2bed209b370705188", - "0x51F9fAE0199f65445c9C3d2429c1A5672ce5B226", - "0x4463e9a525b0A851FAa6398b16756F43e64A36c3", - "0x5E42Cf1d478F52458885f75df041C6246ab272DE", - "0x7682161c9fbe3A4065B38c2CD3ca61263Fe27769", - "0x2C42346820795917e7D11579Affa976b5a571573", - "0x1f92b5affD12981Ef0FA7Ba22a802379Fd36929E", - "0x44a787f30b7b779315e13843A877284A708b879E", - "0x271742440a1f6Bd36dE80b8F7900D1ed7DADF3E3", - "0xb4900E989AdB821a2989dFb6171AF6Af225d0Ed3", - "0xf389994cfE868F531c09A9f27Ca9470620d8686e", - "0x524C78a7934e56216C4AA20dDC14340127308205", - "0x72C5cd18A51D53Db34072546A7A38a4f73600d92", - "0xa872a8b67FA8047265ac4bd38022107b7C30F85d", - "0x5C0cb1Fbfed1cf91eFAd1Cb3f6DdD55f33665fC3", - "0xCfFC127C4f491856cC93433cC5c61a18A6DEF6Ad", - "0xE939d186c9dA1F3640a8A53A3df9deb643021Ffe", - "0xF4f58044B4AfBAA53BfFD87f2dCe77a5f0B32a64", - "0x6d0BBe84eBa47434a0004fc65797B87eF1C913b7", - "0x0b36518F2dA8c357f235bE18b0B28392B45cD1Ce", - "0x2637652AC58aDDfb67b401a1beFb5f48b967574D", - "0x4444AAAACDBa5580282365e25b16309Bd770ce4a", - "0x4b4F333E18dcC441D006ebd9bcbF5eF39D7CA1Ce", - "0xa659aCBd4Cc99a321d44740182B6A8B9264b0167", - "0x0b99363648eFEa66689d58a553bb015957083C57", - "0x64D4B9B24DC4AfB5d3084d7dfb02d5602064E6e6", - "0xB9095B98BBD01A93C395cB2e48E4cC256D05EF8B", - "0xB5d47d2E05401C21303903fDA94285A05058808E", - "0x02cE473377B650b1188778A7e75cd4b31F59D8Ac", - "0xC0670E962dc0feC74199E216eab3252D98743612", - "0xbe7019f8333b0B3Cfc707ac353Ebc2123140134c", - "0x68C3cb93A893A893953d654FAA9D2DC15edCcdcf", - "0xCC9799C6933425D68687bB15c817aF0A667fAD87", - "0x8E900D7CC784A47b2f5B4baf0871aaDf792D0cC1", - "0xfd5F1397e32506AaDAE56514bCF6cE4246EcdA0c", - "0x19dfdc194Bb5CF599af78B1967dbb3783c590720", - "0x3C01182c3BD27Df6A200dDF0059D39cC21c3434d", - "0x9aCf8D0315094d33Aa6875B673EB126483C3A2c0", - "0xc7B8e515C5f0Fb34D301313b636b29d866A2e69f", - "0x6Cb396E01f3aFE12E04B1097AC88f20ebCD8D711", - "0x14ed950929C02124eE464e930070614a65316fDB", - "0x4aD8c6d982EA20C6b105C480C866FEf79dDe36dB", - "0xcDC95322054561DdE9B80c2E5A22f8E09BCB80e7", - "0xC00a6614B8030cE216302016e11C6B4140B5e499", - "0x18e8b8Fb3c843121B24e4fE7672fbc46C3c50631", - "0x70a13e91621a580bc6773e8D27DfF896Ba6d76D3", - "0xd6f8f76EF075e9B585a847B730C0531C1354671A", - "0xB5997B4fD73Dbd007119FcE4e24Ed552C1C4cb99", - "0x574C5a477851536D8675A3aA5C76305C8b6ca2a1", - "0xaC963B7ae5C75f263809C0D8635aC183dE9A2a37", - "0xEbc8c2E0b8C4040e18EAbaB8459621FAf1F6513C", - "0x4836bE8C132D02DaE41f3C609b109272f5ec1AA8", - "0x897b5561Bb4e815b486952391fD2e27C648f6D32", - "0xe27597Af91352537f4EA74622B5a2BA6d464d9EE", - "0x119b134E974e20Df79feA161B08970025e4D5325", - "0x3c9eE43b96bd5D3A7060aCE8c98d75736a1EBC67", - "0x2EA2ce595Bd68a0CD1FD34b06dF98970952Fe4D2", - "0x1C6E47D1EFf818156781D8C0155153CcfF2E1aEd", - "0x0EcA5Bd7b78603C3A4B3292914C98b94500f843A", - "0x509dcC51971b5b77B91585553F3e7C2527c92460", - "0xBF88D8258202F26e92acf7f0349c12c6AcAf4199", - "0x08b68eB975a124a895ba94AeA12AFFf4657f4dFa", - "0x544a40955bA1C7e56E161a59E1319e3313C25251", - "0x6547835644105F1313FFd5194f467720837b0c8C", - "0x7A78Ee74D3FA77Eebf56797f9bB3Bf06b6878154", - "0x103a1F94Bc38313Af8724777Da71201e40ADeF5a", - "0x8a42c32e61242B8c7a69C34762F35E5142a016f8", - "0x5d139aC18DF75fBEBe6d21dcCF47dc54CAd74F7f", - "0xDd1aBFA095818D4a26C02E8Da45CaA5291526aDa", - "0x0724Eb4BEC0bA47CFb808EdC582eebe79B46B508", - "0x939714d0FecF95D77d068838910cb6ce921a08A5", - "0x2d03391bea073095EaCC4785aAD7e63007603dD5", - "0x60ae40235C11116fB74a1ecdd8A175B4f955A1d5", - "0x510C5E29E7d7ed78D4A7D81D98BFaD8A2a67B45D", - "0xc079acf6caDdaa3Ab905C5Fb54330C937D19F60B", - "0xd94AE278E5907aa734Bb895A3729AEF074103891", - "0xD8EF72f4F03615d94993C4a826eB4deDA1F9d645", - "0xa14a711E5D6F38bCCF81d703cb8a949f9A1D3cBB", - "0x8C1f48a0DB29aeaB0E0Ca7214C92cf6CCe877279", - "0xFBAb220C53a3e4B45f7275d38139d3EB3E3ff8F9", - "0x8af7810012012Ff02f0734D46d09Ec1dd058cAe8", - "0x615edf07705eD6b2D5Ac012f780236DF898aEA32", - "0xc5e81cF721038d636fD089160a1aF5575130fD16", - "0xbF1Bc5964f7d035f83F159F00209ff4352BD7394", - "0xABd9221a59ba5c03FdD655834F07F44818979698", - "0x99b25F2B6A5CcB8dc45EA07041F6CAF045D8eda5", - "0x28fd174fC197ba46436D2Ae835d74934e57c3033", - "0xF17ac82CB2c92853100f150591973857B1b48D7F", - "0x3838c954D0629918578847378Ee22e6778473239", - "0xd8855738cF546C6950Fe0AbFa72cd4A98103f0C2", - "0x501F4860aAD23FfA53992DCA8316952e374EAEAB", - "0x22f6D1997f614aC2a9CE43dafcC92B24A9D97b7E", - "0xC57ec17511BdcEF72947CAcA8455127aA751c1B2", - "0x9Fb470376F12AF151c9ce88bf7b4acaa13bAd02B", - "0xe96664029ad91B2D9928bbcAB585450f78EfF8cc", - "0x95fE3708d68B83ba6A9ea6221d6f64838658EBA7", - "0x386D1AB0D50764e135b10247574264Bec20F3E03", - "0x04704EcaAb8f063b76a6f2Ff45D2f414F0aCaA68", - "0x11ee7dccC1Cf223318eFE308ca55C282F1d355aD", - "0xe6445ADFf4178693AB0AbE22ec1E5F44841d1E3F", - "0x6E88F95C51AEbDac258994490D133c95C871b10a", - "0x605088609f9AC2F1F4571C4Ed57a9318dd381F0a", - "0x90F15E09B8Fb5BC080B968170C638920Db3A3446", - "0xe1452baeA1eFb49489CF642021C86480965C9e03", - "0x06AD2f93B61eB355e6e27D9eb8C67EF8eCcfA50E", - "0x21B8d5ceBf0Df88a151c50bcC06465B5E99FCFdb", - "0xF59cDA0Eb96D6DdCD12480bC31c9ABb75eBf1Cc6", - "0xa28ac1264Ea4Dc2fcAe85C379A25E24B44A1aAe0", - "0xa24B12943dD600052bA36A2543f3598C5B6baF3B", - "0x6c2693F5a936f37eD03CfA8465bF2D8BEFf19A0f", - "0xC14352aD978771051F46A3c8eeBd43Add56A6899", - "0xCE8Fc6755EfCaf7F85C28901Bca4F4b936591542", - "0xBfA631508ceEF09dB02F762f7674bb871108Fc43", - "0xCe08958266f58b638527B224D8b7141a3ff9C77E", - "0xE3E7620168DF930e62fb8Fd04D6E8049892Eae1D", - "0xC3dcd2eb5D52fA4A870a69E63350Ecb1248066E0", - "0x7B84C9d300551AF54f1b415D5bF211f852725dd6", - "0x7E9d2Fa27745A7a61b5B433D6e413496B2Da9ee2", - "0xe4e8301296bD883515D539490105b1C797C52feC", - "0x0000000B4D325bB539676dAC6ec3413d5974cf0F", - "0x2b597e53BEF7a9275429a3d5443fCaF0Ab12C26A", - "0x015Cdff2E472F657E8c34B4e88fc60f16f7610ad", - "0xaF184b4cBc73A9Ca2F51c4a4d80eD67a2578E9F4", - "0x585491BDfe0e82edc877f7BA5f9294dcB267c093", - "0x49E59dE5DBF06ED83116AfAA0570Bfe13a8D5bA7", - "0xDAdeF83811243471424558e8d48b85bFcDdaBa2f", - "0x4FfDA342DB6366E564b28E98DCbc48890ae204Ac", - "0x21d9c1d2cDCB44b04e6DED64Dd89bcf5fcb09cE5", - "0xaf31Aa6248Fdb61427D221D16B8C7514F1425C2a", - "0xa762E2e666e70da57fB27d48DCF16b64535cB170", - "0xd09e3d8CaFE386B13792E08752E442669a571f4D", - "0x08D8aeBDE1A4FE3ec84678D4dBa286762518803a", - "0x523d0cd051E272856a2C925C08a5FBA8425c4944", - "0xE751D56c31DC1B16595a2CdfC94388687E26a674", - "0xD54d23200E2bf7cB7A7d6792a55DA1377A697799", - "0x5Cee202B51518b0E4675bbe129F6C9776F4417df", - "0x475E80062da060826A54B3e29776eA55FF4d32E0", - "0x9aD92F7f5EF6F4bFf83c2e10298A927f0Ad7952A", - "0x8d67D01Acd6fC40420F512C044BAeA0f0Da0056f", - "0x4267Ca7c40D73ccBB532fb0F2359eba7cF836112", - "0x09e63215D0D7B0A9D45E1d708A164A5e8C7d8176", - "0x3d80F82622B0Da34874fdbAeF5553a00ca31E623", - "0x804c3083d6565426a35482611a514948AbFb00Ac", - "0x0423275d2ac0A164B91cf8F14Bce45fd6898F563", - "0x5957145FAb56DFE5Af988A4F55EDa4B205922C1d", - "0xACD327490EEB54cc7d1C9c28e3B5A39F0e472be3", - "0xdE698282E354705D609c0BC9067174160b3dd5E2", - "0xC9F7113042615cE4796f8CBbfA6f42170D908e05", - "0xDbc03531e5cF492aA0F0668618B429858F0d3125", - "0x23F1045cA09E9c3206851336a2a05d35bBC812d3", - "0x2a4E02673c3a50C4D5Cb1C372a3654E465d933a3", - "0x8a46c80E07f946662E80c237a3Ce07bE60a42d61", - "0xBD2eAb5074658C0F3FF0eF7045563CafdA008bCB", - "0xB329f33D249662B0F2980F823435FdC7aec3EFdA", - "0x05a2e50C5E4d724897b67b708db432A38c985f83", - "0x149CA92A98E41DC669d673Bf925085567137B7A4", - "0x4a795509a521d22123A66317708698e81d147714", - "0x3967D78660bCBc95c625d58A40C42Ce10bd905D6", - "0xBcA4D68BE543dCEFb1a8bcCb519503f9ba3f2026", - "0x1709e403F22189a289fD9E9d43652d6ec92Fd2F9", - "0xA63ec0012Bc7334Ba5Cfc22901748BDDd854BeE7", - "0x6fCf4b90fd058aede5c1f37f84940c5d935f0369", - "0xa6910BBE603751e19A742154F0986efcf7c062a2", - "0x776Ef1f7692182426E98fd5F383e5ea10DB5e397", - "0xa0A8338295e07E11f118DDc8e3F604b84cDf791D", - "0x0FeB0f31D67ADBBc950Ac0c5266490A9a9735a49", - "0x352bF5e59A4C1a0E4403278fe5C9189F63AFDe3C", - "0x95A3D06fe7B39399E2edcC31435a235585FeC852", - "0xAF7a0afF1E68B9584C8f41093F9977C6E6297a13", - "0xbb1bf082b1a61131D3345c1f99DA32aF592Da2fB", - "0x15B1835430a1fFBd0dc652Aa828a8c2057A82CFc", - "0x8E789fFEec91C349c6699D3F3A4164e45f59C9CD", - "0x6D7fEA7c2A257048032290A8D4A4B3f966F67c1c", - "0xdCfCC77a2e690141432561a9746c125Bf6704380", - "0xddC60D163015CF5Ca1369D557A2ca1cC2de1F1e3", - "0x4242187941A1A2d528Ec591Cba6BCc4cd4BCBb5f", - "0x4809Ecb2fcC97477e86C0cD1499E9a102FAD3507", - "0x9a82aA6eDbC473a883B8906Bf41084c9c1B760F2", - "0x1A2B292661f0102C5961aFc42b278fE8c9169234", - "0x104BA5aB64e949E05Ea77d398DEB30d16c713B9d", - "0x97ee4eD562c7eD22F4Ff7dC3FC4A24B5F0B9627e", - "0xc046F4a055173832866C0010b15698baCCB7B7E2", - "0x31DE2673575c1b0973E628B8bc62Ebd0f4Bd886c", - "0x8c4EC20aa10b617Ea6C008bF7a94Ec97Cd643bBB", - "0x3b3e8C9e5ae7Fd7226f55e12c8F5198641f65aea", - "0x029fcD8016fc878A674ff8D7302a541FdA8945b7", - "0xFf3e84Ddd3e8809591c7ce584358A5A3f4aDcCD3", - "0xc12DE812ae612B6d514b52d529F97f6Acb524c8E", - "0x6fC63168323527883D7CAd0d6B4Bc51932c22ed3", - "0x1A0F3ff515f8a0d353eD7b0eED6292c7560d221E", - "0xB5231E4eFFC32aE4731D4c6ef65aC58E116c1EF6", - "0x952cF57D0518BC03019d397e06a5a9dFCfde2caA", - "0x7d0b45dBEf18ad95633375236336a9E7B9Af7d76", - "0x27597BDbc3C8B3E865e2CCeC81314802FD46f8B3", - "0x4774b0e8c77C06A3fB636243d63eb085B4F6150B", - "0x40908EE8c5739aD1eC1031B480Cd88372BBc5c84", - "0x0815e5B76416Dc593C5E82Ee7331402d7402a5B2", - "0xc2f44E7F63a23a1A3d75146425C3125Da3AE24AE", - "0xB4C93d0A04D4B7966776cC2A0Ba31b2C9Bd40d18", - "0xB118FD9852ec6762ed38799CB43F0Ad1f0d29a2e", - "0xb40c28F91Aa98A2234Ad9E5693FeE052cc173784", - "0xc766D73BE01Caa06834032bF5C8F8f0F41A7BF36", - "0xf6F441A3784FDbCFCaeFaEA6881571CA1eA625A2", - "0x88023c305b471649D326b7B95149146F4B5308b5", - "0xA72e1756426100c6207421471449E2Ba9A917e86", - "0xC6387E937Bcef8De3334f80EDC623275d42457ff", - "0x514c4BA193c698100DdC998F17F24bDF59c7b6fB", - "0x51406DF329a3E1145dA4da6d79143A3747eD51cF", - "0x88fE0288a14499b9A42B10fC031F91eaB341a5c6", - "0xB3bfB32977cFd6200AB9537E3703e501d8381c9B", - "0x94db83f9934adBBf5966A30e55b83e6635c745c5", - "0xF73Bd5710d3e3d4e8DB7EC6058F9fb77ff8414e9", - "0x1dA29Ca4E330F0Faa816edB6B55B46f9eE453F7f", - "0xe85FEa97723d9a67A9CB4Bc3E58FFDD6dC3a78af", - "0xf8E98A6539545590F70Ea8D46D5147118Ee139e6", - "0x51d18E5F6FA6534bB2fbAaf90FD6f3336327fd16", - "0x61E60af04805D7dDFB0CFDe0A96A3b1C15F3748F", - "0x5Be51ef6A7a4611c685c5CC40Da81C73A6588039", - "0x33b346911A75e8CADB1D3bfA1E9139db59396867", - "0x02a84163763Dd791328765b96aed43b6a915af48", - "0x609470c2f08FF626078bA64Ceb905d73b155089d", - "0x39F9bC86f9fD445403fe1698708E76E86078Ff23", - "0x88cE30F11723e38fFaFA91A22462e57222433467", - "0x395Ef2d7a5D499B62Ac479064B7eAa51ae823A22", - "0x99298b15b5dDA538C67F08af804D85e82450Ce39", - "0x14b8BFFEF5D546727DF9A4357e69c49907656179", - "0x2EEF4219055fd3C514Ba7b50316f1C3965A53751", - "0x821Be125973279e5ce5eACf2F9360dcaa9caD53E", - "0xA582da012A45FC26a61a134A720aBc913938eEd8", - "0x4F725e7CD53D76Af3DDE36Bd05DD06292A6F3c56", - "0x26c659E023E522264455374F82758E365994C420", - "0x04bAb032593c39C7d600DBB0e617136095Db47fb", - "0xbfA700eDc21a6Bf09d22Cea1BBA95FC54e7b6dc5", - "0xF09A845C973b8265c8519D503FA6097caaB7f932", - "0x73961ce0ac9220fBf71c86BD38daE888F75bB169", - "0x9cAb916493D31D1114a1D4F9a497aA886c1506E9", - "0xfF7D94d50aEdb1ECBa19A56F5A1EeD9371071eaA", - "0x4D3A307aEc8eEEA1ac093B1b2F4Dca8E23A064ca", - "0xC4Bc1d401b997c7855F4De64d197B41Cd2860aaC", - "0x67b9D361759015D38B43577E0dB6047D4737257A", - "0xF5BAE96e458298d5B723aA350E3BeBaf4779CC24", - "0xE7B80338fE1645AF7e6e1D160F538E04241b9288", - "0xcB0BBF3C0A6AF3788C6d1b108EC31937C2779df9", - "0x4c679D206d5C5927e5869B8E5e1502087e185D21", - "0x702c0Ca3D5F5f057671074466311C5D780413932", - "0x8008145DF53e1db0F22737523B029320f6922e41", - "0x9abEebbe3B9eAa8b2156E9989C86aC923bA10175", - "0x51a4047a0F5CBDE25846F540aBA39D5F451ac885", - "0x6608673B607C5F648C347014dF5aB1A492D7281a", - "0xEC4A6f59960Fb55A7Fa49262e2628687b322cf62", - "0x96e4A0657006891D4BA5fB745CF8587ACD1B57F9", - "0xA019B8ad2cC4B2dE16E42cce83aeCf8dA5CA617F", - "0x97af0F60C1894Bf63B440300DDF6b18102f22341", - "0x22138A0CFf84952C018cbcBF5650149017d6b292", - "0x26FeB72790987B6958fCFbF1F53946745F2d3a1D", - "0xBDf9414544ffc5A4A72029AC6511B63CC00F224E", - "0x8b78eb724A22B422891d048012606B5563E62924", - "0x5E4eaE80115A58E3C1fbacaa82107Ef039E3aC6b", - "0x3f52D38Ca3a23c67463f822854D95Ee8481EDeDc", - "0xe66B290105F73dd5f38b25c7d4c86d2C5E5a37be", - "0x4ed7A3c71E9Bf0009317ff24Fbd8BFe96D8D808d", - "0x2445dB49Ce5e58D97a2cb1AcdB8AF326AA592F8a", - "0x493409704eC5e2eD28FDAd4F48638532228aC693", - "0xd8BC0F7888b26e1c71Ff43b487e8C76871D80Eb6", - "0x17D5E950cd12eAaEc810DeE26D9355710Bb22526", - "0xDc5b17F7e80Ba5f2C06E9d04bd202d394165E093", - "0x0b672F8eAdc1fb93cE72bA2966a32533D91c7293", - "0x6AfBaBD050ee883a2E8125d367E5f7F410172E57", - "0x3cdc6F91d41F1738e8E1cbD2A06F64dc6Da5b0c0", - "0x448749280160D2126053330fa7a18f893943C455", - "0xce1B76A218F2d93b564B73958aDeD0e36885c4Ab", - "0xD124524B52053c2710549A359e42d96d1Acf267D", - "0xB2515a7221b2654F9Faae0E4eD1d0E49Aa7B85DD", - "0x933B29E605d3A5461849798DdF0A39100a4629cd", - "0x87664BE9494Ff8B8041E13e338D586e9cF6F1A10", - "0xD80DA4487c1ad77189821dc50696B9A619ee39A5", - "0xeB66A524CBc41999f142B4Dcd49Ce93e8CB050Ae", - "0xdc46Bb979Bf5102d78ece8e9A91DbAec932e756a", - "0x5305095cF75CDD312C202e27F6aAf3CA7da79A5e", - "0x856A39Decda9480D2486A166Df556F78510540Dc", - "0x24617Cf7bABb16681184F9335834a7f053f42612", - "0x538F28B757fc0412A250491e586838E2ea240C54", - "0x7be928201cA49a2337E3951A1372A6Bb26a2D058", - "0xbb49a68c8EA9C2374082B738A7297c28EF3Fda26", - "0x21f701Ae5ADa0d8A702947460488A1Fb43A3Ee60", - "0x0736EB5202248125a869AeA2eC3b15a8F0Fa2Bdc", - "0x92D010aaD4fd7Eed3f8f1B61701aEd63c5d94B9c", - "0xb8aF84C7E9f3731Fc4b8f4308f6BDc5cC5e31307", - "0x8F30c5a59B5284530f57596814b2b2395197E7f4", - "0x39C22b7A4C3aE4Af955D2A6C695d6C40248339Eb", - "0x2B185C762F6Fb49a07116EC06B238B58Fe2BeC53", - "0x66C0d9152209B51977047b9DC3b0B5bf2339b67C", - "0xA3C6312795199EFE026912CDFf4B4e391a7F17fb", - "0x2b05cCa9ce08053B8aA1c3fDC276D41a82c0Ad36", - "0x0B97f99Ee46EAcBFB002761B69B1b766DF0c8FBF", - "0xCe71D1b2443c8a7207965d54dB0f74B29261fA00", - "0xCB6222f4Df04385ea08E8DA2A5871131FF5F6cBA", - "0xb535d4D9126c58d8dA8fE5775088aF77DE37F5D7", - "0x9454f17a6BcC36CFBC8A07011B33DaFCebE4050b", - "0xc2171F490C9eC7D5c6c75cD6d9D43C89Cefc6867", - "0x66cC649D9218BD1Ca46373FEDD6041bCfFB93640", - "0x74Fc147dCAcdf2680c3219c80191121f2Ef2258B", - "0x560D03F2b20e9047714Fea87Cc113D95a3fc7179", - "0x2a724bE1b63caEC1c3Ef95834fdD443c3347Ee1D", - "0x0aEC243F82fFd017666F6ffDF9fA81DAf2546fB4", - "0xcA94F36372aac4B951bA369B3dE5E59cF8CAF1e2", - "0x6CF51FDeF74d02296017A1129086Ee9C3477DC01", - "0x9792E2E8F5a07Be11b0dF35B3bFF61932bbF8456", - "0x3f5E80115268633E19D97D61a2752E1271409e01", - "0x3ea4ea76eFbc4da7207635B3705654BA8D674a62", - "0x126ED947de7b84ab29526D35CEF99C9b72B285A5", - "0x7644Fef96321e78d0158b7B432ab9C58122f319f", - "0x1813c5eB6698250fCD0A4BeBD06b9Ed8EAfF275D", - "0x88d71c6C9a83C56d5742d82536bccC3CD5348046", - "0x4747617cE6A6094bcb67f3c18039Ab233B6e1164", - "0xC6a8d31BDC07860f12e22C615c103400b28176B3", - "0x258302D09995A71B185097738a46Ec2095b9197f", - "0x477bE27B2085D890DF293AA23BCf010363cFB2F2", - "0x71b700181b31747f28a7cdE2c26b503A2335c0ad", - "0xAC631F4F75B5799f9149e70aAC3af88658487f84", - "0xf6FE557e83cE862A63C467E904E3654ACDFd0Fe4", - "0x5a052A9928d100bB68e14Ed2A8091C1a758e1BF0", - "0xEeF0b8C8388a332973dFa7A110eEF1f0de8670f0", - "0x37aC09e1640577e1D71E3787297A56b58f88F0F2", - "0x708dF04E03AC02c440937Be6e631FA8B46CB89bC", - "0xd4a214689898eAe23F4A36600Baf0FfF1889FCd1", - "0x8EBD118444D8239505D66c8Cb6Ee36eee6aBED5e", - "0x6a087212B8B9fed39E0874Bd37a5236236EE8aCD", - "0xdBfC3230Dca0b50Df050ac5D3D42BF1eCbA1b82D", - "0xACCb019890BF879d19f5aAB1db068057e4CEA0F4", - "0x9D0a96a24d9c4033C882a3c4A3c33Ced4254581b", - "0x76655FBE50A89333B4f175C87298a6E4BE4D2797", - "0x383697a7feF14483920BA9be57C26115383D1DdA", - "0x21bC5e5558e85E1e49589B4B4fAb796ee446fED1", - "0x32A1874657673C8403059cb30880612aF7D2C9C6", - "0x04ce112FC3832fC423DFAdd4e344DeF6A4840fc7", - "0xb7360b4425BAdd4bf91725796A7Ed431D8bE8E43", - "0x10F73c1eadCF834a33061c4c8a962E528D6ad883", - "0x2F5a662614b84f37F71F25E4F56b7562a0e71786", - "0x8eB9da111684294F6d57C8C400c71D33e1221800", - "0xA82d4D63fc6332ADfb593CF21A649947f8D5464b", - "0x7822159ee394D14745Cde63a706F965fB73c7Ac8", - "0xE2D7C679AeDc71DCFD65Eb381107f8beb0F65666", - "0x1C00268fa3499658124b8FE6789D371c0a22a21F", - "0x82AB77185a80A5B74C92d679a02b28B115F21962", - "0x02aa0B826c7BA6386DdBE04C0a8715A1c0A16B24", - "0x7B230F8235f3E3b14AeFd3ed657479e8FFD931A7", - "0x1e57D64A5ABC4769aFe6c6c01f58f666e219F167", - "0xED34101ec8916fe669407777023e71a70C462031", - "0x9D497F015fC1f853c82C016AF981336ca3aCE04C", - "0xfB4dd74260f74FfDEB141A1a8f209710c5ec9d07", - "0x6ED88188Da646B6fE64022895dE4045C35EbA48e", - "0x5538793f0eD24fD4EA2Fcd5225A40995A8f5Ac3a", - "0x3187a54a100e7DfA4fA60c5977d1921223619F2A", - "0xaE9A5F08DcdD84aa4579e0870A07b62aFf19E4e8", - "0xBaf88B32025AEAEFEE010013dA46098382475DdC", - "0x8bd7AF715cdb9f5d2AbD5dA5138671eEa19EAc70", - "0xcE29cEc7552ab3c9Cf5264Ed96081c2804C3ef07", - "0x6458559c5489f3B9274de69294B8C1AbDDa867Cc", - "0x7D1B10577DA49D490685f8105bEE3fA7A82ceD53", - "0x52B481e0e149a882dd26167e0510bDeD8C595330", - "0x109CbDCf9e4F6Cb9EA56c810bc26F06f8b1F399F", - "0x6A08CA93567d477C6699C50d3De6eaeB0307C8c0", - "0xA3e17CA80549C16CC850D37acee1a02C45736f68", - "0x0495C779401b2D9f8C56d9d156f4B4b653Ffa2c1", - "0x0fD90eb42aD3726B8E5Dd0604D175f2aA9F10aA5", - "0x35B74170281bee84d414202EC5f16e4B1aF7Ca39", - "0x004ab0F6171FF2d5cb44b6d50cf8Eb2edc56105d", - "0xE4D97EA3FC1459a8B2643A7cb07f7A4471F32F8f", - "0x4D4F26bFF1022EACDc8369AA12BF800211BE86aC", - "0x3A39899DC78b7307aCd83f51463C853Fa79E1B09", - "0xb798DeF92b18861602f956D06fa5CB0bee47eBb0", - "0x7A4E47a4E99e7d5Eb9BB5A619E4475D1dc7B3ab9", - "0x18593Cea6fEC4C6351c42f99f4daeD0B2751Ce41", - "0x79A20D3b60e7F397D6C870C21335decB105Fb1C0", - "0x31345e8a872C5a34e6264b1c2aD3fc8D13a0734e", - "0x5192Fa6618869fFd9Fe6230D94B0e4C8F57ADCB2", - "0x1c93bDA3BEF1E486ff31C745278FC3Dd0aADD6cc", - "0x6169143e0f2452bC5c9642EE64ac61a1b6c9211A", - "0xa12EeC92d8C155C8ea9159Cb87da4e062BF9992d", - "0x0FA88472c52491DE94d0d40C4EcBf69B63c6E300", - "0x3352cb989e5cf23335AF7f556E15b824f7e2463E", - "0x175079d96B783C9e9CE35B33C2968b683531ea6B", - "0x06Da2826dfa8d4473DD2a87Dd28fD186c81b0073", - "0x253e11bd6b8e8f302D29622F37D6365a27087306", - "0xe139040AEb075c3F0bED81bb66F567b5AF19caE1", - "0x99525b18Bcd5F429e25F9c00863A917909cEF3D4", - "0x8D7E07b1A346ac29e922ac01Fa34cb2029f536B9", - "0x7385f8b5ab1303C8E476d371973DB768F1a43Bb4", - "0x52a950D98f11fD9eDce6692fEe7cc8e110A471dc", - "0xC2945Ab7579b4c05Def2cEA0f404268fc75952a2", - "0x584BBb33b6Ce22DBB8BA8c3CB02F90a88F88451c", - "0xf4A2f32BefE6eBE65bc2696969cD1C1A34E0f92C", - "0x98fa18f31Bbfb643F47Fc866262Aac55c9824917", - "0xEdf6bb53c523Ce45e94C5447afD7a042678fefb1", - "0x11EcFE1260cFe3c4ae27e579dC014Ff83482D9Cd", - "0xC78E8E153D926047A07ED9A8eCdf837061F11B86", - "0x8016aD7A57Fd087679c57E766D0cC896BF43615D", - "0x8ea12BC08b5cB9e0d9A38C90D98b35AfD966C3c5", - "0xf102b03611E104CaDeEb7605Ff1d9FA5c07Ba115", - "0xFe3C6993920Ac33bA28378A9f92e18De52795117", - "0xA40f36d4502a05d215cdcbAb5693D10e271A587B", - "0x4BBA887158645192DAC8aBea73D6aaEBab2B3b13", - "0xEA8d665651D7B3d429fE6bbF6F895456eE2B6B6D", - "0x5aC2Bf9D420505E61a601143bb879AD02F3A3746", - "0x2450d8083B5eC7C2E9c1122085Aa23Fa3539c64E", - "0x7CAA741d741a38Be582fa619eA51f4657d85E85f", - "0xabf124216ebc84F1DA154e5e920A7ce6269535a1", - "0x009994eAF678D11b17151455Cae0C87E0b2B6825", - "0x1867608e55A862e96e468B51dc6983BCA8688f3D", - "0x4a4AbC2439620b311e6E044Aa22841126ceb9CF3", - "0xfB5c2BD48E34Fc530eb857aCd71905e2b17b1F9D", - "0xEB057C509CF30cc45b0f52c8e507Ac3Cf8E78777", - "0x8Fa4Bee3787714f25Ea4E6Aa16b27438FDC0a5dd", - "0xFa4a7ef5D09F454114FF78e7A8A23Eae71bDa600", - "0x3B647c93E1B60F41ea687f1C9EBc6DE3Fe52f475", - "0x961aE23F0cD4CD82FC129563Ae23D81223Aaa2D3", - "0x2F8a99D208b3d09d2A944e27d4451D21EAcEDc21", - "0x5d77392dD7ac01aF37639eA303FA44e980C83f79", - "0xb6B2A09afD721AD5742566FE3f02aCa3a2a6017D", - "0xdDf169Bf228e6D6e701180E2e6f290739663a784", - "0x2Ca99Dbca3B8922b6b4bE71554e543254f11B0C1", - "0xe51D044586E4f3377C099aA67Bc523b3B3cD5119", - "0x39bbE14F5fBb3276d06760C95a071ebaB6529192", - "0x1d0b6015C2aDE8ff59DA0e2e23d0655fd8B3B3B6", - "0x987c63895621FdD6a1e7A6414043b561982A2354", - "0x0f2f90aA248c8960dB7a5e13a52f3769bc4aCcCa", - "0xcC915b7884209e7C6faB158F57F854517f4A7bd8", - "0x3acBC3Eb173FB19A3681c6539a1e108Bb4cFaD8B", - "0x24F5eE0991f95328Bf74b682F6faD54CFF9C832A", - "0x19c4E72F327ccc3B47e6749dcC3920DC232C8DdF", - "0x8CAeF18A4e4c68A1132AE195E628ef68043CCb05", - "0xEB32515Ed999140479893d96b5803e1c0B014bcB", - "0x2dcc372E20e0c9920b4e2d7c51C80EB92271c209", - "0xDfB79Cf0dD1890f13EF73f5e248b9D61172173C8", - "0xD8e51fc9A41C19bcA7646dC054F03c40BC5ec6a8", - "0x906b4a9f69C1bbC14EdC8D141B4A22DF6726592B", - "0xd3d4f998B66F0dbAfE5645D14f001e6852271Daa", - "0xd4fe43Adb09b3753A846b6818A0A50Ea8E4fF188", - "0xd8b2B7F42873F111348c835563e26865474337db", - "0x6d0163c1Cb3F4CE3F7528E2954D59d77Cc8A8053", - "0x1E6bd36167620641cc88C846B815adA76F754e5b", - "0x7267db34b6072080923F92E626C2Fc5DA91fF25b", - "0xd04f2Ae9B9C0A5a80812e901733d799D66C78b9c", - "0x05e5ab383f3C2D423cFd118B83420261ab0990Bc", - "0x181a15c71A37b4b829477Dfb7cf006F0493B6f86", - "0xF8253EAe286d2B3AAab470b8118E60b86021Cb2F", - "0x69ce31018C82CA3D2d9E4C5a6D83161B4320f9e9", - "0x56b32744bb8AAE79f34E7701E581cB6E2136E0D3", - "0x4B240321bD08Aac0b0c2ec441d5bB1f7190Ca0a8", - "0xCbD6b0DeE49EeA88a3343Ff4E5a2423586B4C1D6", - "0x3E5944908451231f22092218129f4b7D525E2E61", - "0x1A5CB0C69C2e607CCb6Dd7FE7BCf394636A4215F", - "0x0dD16908663523E03C11C0dC14FA4276C1010a6b", - "0x10890742A1a20A936132072C20Ae77b081486190", - "0x25E668E561f617845F044267AD0dBbdc9aA36c6C", - "0x43D4CF2Fbb008FCe9548d94a35C3F318a4940055", - "0x8A30Fe8DD7C1B8eE6fff8892731eD345b9E33B29", - "0xa3D15d2EFdD2A93e5A4BF07D69efE4A142A347f6", - "0xbBc711EB29527E2b86049d41B867Cf009d0F1453", - "0xC9B7411fd802BDfA055cFD469D1aeCAc2Ac64394", - "0x825736E7336232CC595C345D049474226Fdd154e", - "0xbEB439195367d87184733bAdb1f4F26A7df9C576", - "0xA016B59Cea666f53da4DA3C9F99B97dA1e0b016b", - "0x45B6209b736912E6C4d12D3650b582D581B28978", - "0x7911Fa8d5bfCe62ef68B714D5b51A65A5732B101", - "0xE1666213AaD05DedfF07886813fA3b88F7Efca21", - "0xf64a67a81A0f9bdB67fA232918859Eb6c640DD4D", - "0xCc83DF78030Cbc991df847f353b189fDEC97D359", - "0x78fe849a5e90A7f5EaDe2b4dc8B443F883946d5B", - "0x083df0282Ab7680098af6059F0422Ef87900D22D", + "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", - "0x0c9C3Ba64072eb566b0E9A4B6Bb0D7B204d68469", - "0xf38a9B66F90a34AE03e21d75F398E60Aa895e45B", - "0x488A6A10ff29B278f03575d466A1F9D6bF494ba3", - "0xE399ed9a36DCd2f39cE0398eAccA536C4F2d9d1D", - "0x19799F272574079A8DD591C13F7aeD0d2f7AA011", - "0xfb15339d187A76AF62698F73D700E1F8536f03BD", - "0x30A26c2837e9Ad41Ea5955949F00402DbF86f124", - "0x8Da3280EbA3BFde4Bf096b4E7b3Ea45ef70Aa3A2", - "0x7543dd4EE62bb86a04Bc133d0eDD3f19fb711324", - "0xC345C4d05C0ecB55A2946Ba0D2bc2e737Bc9a2ec", - "0x5Ec850d24F1a5fb122Cf9b3446f431D1f44a4119", - "0x0d268e9b0DC5870fA1188bA99feD52eDcc81A985", - "0x3214e141bdAe08Be1f382A885cf3d2a448A5E780", - "0x83fE7223CC6Dc1430DfeD5435D4a264F2b799100", - "0x7c1A2171553460219fac450d93ED2051f61165B8", - "0x97F0d7f9d9e7Fe4BFBAbc04BE336dc058873A0E8", - "0xc283E7977583B2d17353Bd17d01d953A770A5776", - "0xaBBf75A59AC8838FA46bd5260501B68ab28B95f6", - "0xFa537C114EDd1D85a6019222C1c0e90707d2088d", - "0x5AAaceDF5a180e084d5eBAc2E55Ea484cf0d2780", - "0x060e02320fc82a7Bf148a443C34920204d56E7bd", - "0xFd1D918196C4586a7D58cb75f1aCAe330Ff5f348", - "0x7a44834D4dEC3F21a341dE51365fDefdD41faA98", - "0x9979375A29d542A16d1D7042b143C9Eb07CEf0EF", - "0x67c4064fd8cD5693458d5efDcbF93DBF4036384F", - "0x0CA1Ed3c27687a46DC287DC6ebB4974AE5A0c3d9", - "0xD01C4e5e9681f348B9CE2135ccb2e871081cB239", - "0xDdEbbf1bb417E4a2Ce7E1E0Ec82ABD77A899DE51", - "0xe78705fA043687cbD7b907d91730d1FE5CE66365", - "0x88e64d1eb51d4dc955568900097F01D6af953a3e", - "0xb9999D5f3AACc5c92e1eC4FBD31139a91b24FB08", - "0x99C04ae385E3c247D8AF67b193357B79ade10cf2", - "0xa9eAEF87c01B4C46a691862c7Ba94401394B8B9c", - "0x2408E836eBfcF135731Df4Cf357C10a7b65193bF", - "0x0055D4369a59bc819f58a76ECC3709407204dbAb", - "0x93adaa5757cb116cbE445f41E65108425D5b102E", - "0xf38b07B8ac72Ad70806E902c2ecFb7EdD36cA3f5", - "0x89D1a663b53D5D7831f67719f2fe4932f8251F9C", - "0x93a4a366dE322dCFC68b629D6086C6b19Be4aECe", - "0x84669952f2b6309B7876265e7037795f5958B388", - "0x66697dabFfe51a73E506f463b27E388512ed6Ecd", - "0xe116d5F4CcD817e93a4827B4B1166fB3fA61BEC5", - "0x9F9692FfF5Ce2C81737f62bccA101a7a7bC31c46", - "0x4cAc5d5403996Df53c3b2ff5058415665dB2Ff18", - "0xE434109eBbF886FbFc2a364dd7b78BD3d79279Bc", - "0x5952bAb55191f8A815Ccd8BA4945F7a09Ef5DeF7", - "0xf90a71D46DB26a7A3EDcEe7Ee2C93a10E79EB5CD", - "0x19879714826aaB01A7cc90eB7C254177C460A36A", - "0x83d880Ec71546746D24e7fa28E1eA3964E236d82", - "0xd0dC07B98769f23A7BDbef15A35Faa256CB65dCF", - "0x0FE1AA632A2837239f218e3Efad21E8018A9F4eF", - "0x6720Ddf5cd112a57Ea30f1f16B70f6F213AF71e8", - "0x9be8D78f7733681189AdcAa5634a2dcB53fE29Ae", - "0xE88a832323ED7B3D29Def7Eb357AC308Df3673c4", - "0x93617280d57564241b4aD4F4e09e02968F3F2D76", - "0x1D5156BeDd77567E2246ef3B77451b145C77A347", - "0x27936eCeeFd9F1e30c20F13F7BC91cCdcFC2D907", - "0x84Fa205AbFC260106765E5DaAbb1346Db354Ea74", - "0x190ac830b737AcfbfA819F1D3aB0a6D702D72147", - "0x8a1E24491a2B76E94F69bd9F980D0Fbb8c3B75bf", - "0x9Ebc0B1Bccb2C2412B661510F2b2e67828347F0C", - "0xAaC12ef7082D58bFff1E0796980370A5BB544196", - "0x0DD4707643294200BBe1115D7E414E55F99eA61d", - "0x6c7286c5AB525ccD92c134c0dCDfDdfcA018B048", - "0xA60AAdf62907bF06Dd714781d44Bbd4C2d783259", - "0x6ba58CD30014A861b11eD429200Bd1DD8277DCf7", - "0x7287c3d93b89B7f9153fEE6Ef086Cd2858e9B9EB", - "0xF1D508C2cCb2De16B20c2e07869408524E1a8D45", - "0x2c2e209465D5312e6dF0cd5F7D1066f1aff9a953", - "0xf626e9A2FDDbf55b0b1A87C56128A7ba6723A85a", - "0xD08B4F65Cd97123348F41e684f11e65bc510157D", - "0x0fF6E1e68413D2407Fa6161ac4Db8f21f4D3aD41", - "0x28aEF7e702afeDA80F05Cb8457d49cCebe7F66f2", - "0x064C3BF2B2A26062aC038b95aA2b223CC8968040", - "0x818854b363b90791a9eBc29e2f9c7f1055ee5A4D", - "0x77c4a515d6779f358d9C70b78c18715EE352CadC", - "0x170a426c949caF6248Df60a9B412eB3Df9d32484", - "0xC2c0331897B98c72b092625c473867359Ec5f6cC", - "0x3381Da659Df786762a59B658c43c850188808205", - "0x9C1dCb699e6aF3AE59b7219e5EA7fEf6D9FcF9C3", - "0xa13910B72ce4C31F08c84842ff0a707f1c4Db389", - "0xf6831d052c9dc1523c33E7BB9B657a33AB84D5d2", - "0x9aC63a553219198ADa836731B2aCC1596987B417", - "0x62371B45D283E13Ec4C6666D3f8f0Aea06dB78E2", - "0x5d81d1b2775794E5BCE61Aa75FcF8778D7d1ac89", - "0xFe6E29c244DC1D06C26033fFd056e0f81224b64A", - "0xa38F13b62142230F01842f63A014AA59652EC1a1", - "0x2C8cDdB4Ed5Aa7d28fcfCc75313dE4286e0b5e32", - "0x53eB9A6f34e1FeAD9Cf1013c7058679bcA6Acc43", - "0x39B2bcbb9fA29D72755AC2446C310FE98C065A12", - "0xD80267de5eD5de6F7D6C66986b17b138fe2e1220", - "0x9E96A27867464C9f8A48935b143Cae922DA949d6", - "0x00E8ecfd62A04cCbF2b22E7fBb6992900151ff77", - "0x9d0Aa99Cb5E64D4Fc6203906c02eD2409c9E78B5", - "0xDBF4a4874E5BF633D8628e5272F9965EC1Ee4dD9", - "0x79F38d7d21e8A3d2E91aBc2Cc2eA6747DD5153f9", - "0x7f965552C63da3Ee12eDDF79aA74B3eA6bE16D2d", - "0xA770C94a39D17075ca9Ba359b9ee191127649Cde", - "0x3b46A777dd4657D91eCc9ee3eFea1162AD1cD9F1", - "0x97992871D78116A928aDD496850ec410a31Fd564", - "0xd12630b70D3c4Bc35D9a9B20C1Ec7493534B400e", - "0xb334C11D1612231B60aEE4048fC6B57C3ac91F3c", - "0x4F00a16d909E3C9DF5b5e7be30b2aF0b0E67C5a3", - "0xF28570694A6c9Cd0494955966Ae75Af61abf5a07", - "0x765fD440bAcBd1A34bA08F0ffb823fce62F06c2B", - "0x0BdA0F33311E65379461D0A555BbD7669c0eaA22", - "0xdc0D491BD92861746C745b257edfd8Cfa631470a", - "0x88BC11AC7eCc805b024ffe559F44CbE9957c21FD", - "0x7D3A0eAAFE6CBf41bCf9C2f091065eAc9098e0EF", - "0x37DD83EbDE2d144bF68be9a6686A3c9BDa6Ff73d", - "0x57C6bDebe4702beb4Ab070c903EdFBEA575E0923", - "0xc1A0bC536251215FAfC9ccB610752D8453545138", - "0x6542FD1b7d7584f446b13564b0fe5214C8b85bF7", - "0x78eE7f30a9e1A2B1BE52E924c9fc7b608177D772", - "0xc9C9077be44B06fF3Ce957D72149C65128F14331", - "0xE8996228b2A7A7937AF6eCB2bee5b99bAba6aF3c", - "0x94bb64f974b5Cc1aEEaFaf66aC43c6195d6e7BB2", - "0xf51e5a0A85d29AA0508894405D734BcF044dcb5b", - "0x19Db2dea42083d3b30Eb89B1c30ac5A7b91de42B", - "0xd0a2ab6A3bF6904093EC8FC5a8c151E3c4543251", - "0x747276019e3340104C96397bF6537aD01F93D7df", - "0x51256EF00c2485F3A40fE1b6E4E779147BCDbb58", - "0xa24b04B313F748cfbA45E2C78FD507eb85dEB17E", - "0x6b9E4F287d44cEa39Cd8CcEDAA42488617AA21A7", - "0x67bA1C914e6b3b396346a6498C1eF8e59802B1e9", - "0x885AA2a20e2e854A5cbd8aDa548B7f4A8FFaA0C0", - "0xf903672BA62039591812141eecB4CFd9416CfC83", - "0x4538Da8BC4108a1f1c20eA04fA1ED33Bd141d3b1", - "0xDD043889435603F2B99A64B99A065711aF3C02C9", - "0x1E9264e33Fa02406E41De611962a72653EE24Bd6", - "0x161e82a787324AcfacD0D611faF9aD13DfaCd65D", - "0x8c01b3606b5466389cdbA890Ea981C23931042b0", - "0x214138492F53fdE449bFb4e91B91e3e1bA342264", - "0x9084249d04B91988713c24c0EB10D25b90B4325b", - "0x920ECAaDE776B466A550EE3FA6dc860f1A78f78A", - "0x69258d1ed30A0e5971992921cb5787b9c7a2909D", - "0x709Cb5D672CaBab9D50250C0Af982c52B1b233a1", - "0xDF2dBAb86F0cD5eF39Af65cE8411f747F36a12bb", - "0xD7E17834f4aEf24F732DBb6f0D364ad5FDE9d516", - "0x4FfD0A59a26cB2Aa76D403215e4cC2845C053994", - "0xeC898Ca8A76FB722A1Db369E69CcdCf02f31f4F4", - "0x393A256dDCB830a837fe821558E342D096a5f54d", - "0x09dB9b873Db84AdA9059630f79cd832b4d576800", - "0xeac4d6F1858e6AD8E99C37FC390A4cbB4d519d62", - "0x559C027192F28c0aa3f9F531324C7dADFaCEad82", - "0x73E8D574264ACc681E8DcEbA54f36F5eAEDa7005", - "0xA07BE4744Faec9C69a92D654F5d2332fa73Bb87c", - "0xE57d3adCd6818b8e40BFcF4EAd289AfaF4D40b5B", - "0x5BE23238eBC1A79914f4376781c71a2B37c49892", - "0x42Fe01565e60D0687Ab793dE8caFc1e8a39816A8", - "0xae3D9aBA194740091EdBEfc8619A90AbacdE8fC3", - "0xFaC23A1391b42a1BC4A17F972be2555236003870", - "0xa049AFeF83d112F9B9Ac4E9d743C50aD08EBEe01", - "0x7E028136AaF176B8E338581C0e62857d8b7f5ef4", - "0x47C869583F24e7c187F4d57CF1c2736dA51F145b", - "0x845F8EAA68D4F75562325089115B470080F04052", - "0x5d02c857E98465f5b3a957B1f43569C4dAe58cA0", - "0xA2c12dd46eB80ad3EbFB9F07B0293b2e076BFc5B", - "0x88EC8e63dB2f705e518e628Bcb6cBBb0A4f170b8", - "0x0A83985E4A6E8Dae2B67beD4f2d9268f6806Ce00", - "0xef252a2c56d61D5417BD59E2586aC0aDF3ce538e", - "0xbbAE0E0795294Bb37F9B7CEE715E7F15602f67DD", - "0x40Bb0bE4171a888dA2a6C46aC6aE505A63FB3aE9", - "0x8E77B367fE90d91b3f56Be198Ded811b74c82BE3", - "0xC3A03c3F58C674FC41756744bF2210C8bdbc4081", - "0x7A0e7ADb1B9A40a4B4656ad55102d46DFdD6C19d", - "0x5d8d5184B35F0569B79d7E169be0aeee21A3C078", - "0xDB0132c875eA7A00c4a6283da592ae6500205396", - "0x123b121BB9771cB171D34c835E92F891759Bd79f", - "0x0d2Bab6899C0a058b0f3162F656660A2F41a7712", - "0x8e6ed79c48944265FE5D004010c11CB28aa105B4", - "0xA6cd97Fb84c669B636B86e0C2CF5e3428363a6F6", - "0x24E1C10d4765EE61840AEe6Ca38088023ED67435", - "0x904BeccBDDE4436696f14d846008818495AA616c", - "0x37df7f9254A44ae8aCEf12f9133724bB78F5E5af", - "0x0B997e226B63FdED87673Bc8B43bA24477b8147A", - "0x3722d1a53fBA65dC3C2AbAd6C5Df0f1C969699d2", - "0x333287aC13B0Bb9251464DafFe18dC309aaE770d", - "0x9aAE12dd452f78ae8919C2168717e7348392aD11", - "0x0FCfe3C0d30b88B9b06F85DB087439c524369E51", - "0xa3b926f6d2bB5507fE711847640bA2086CB11A75", - "0x12f787d5833b8B65F9637A5d4bbb163b7db531e1", - "0x1591fFE832548d7ef6152944CaaEf37fe313381C", - "0x229A933de3d977048E07aFC630590a91Ab88c47b", - "0x5d47e5D242a8F66a6286b0a2353868875F5d6068", - "0x8fABCf470152e44E01750d50f3631F538b8C5d8a", - "0xA1CA3A8f81b56407ccF4b8604cD2bec4E005d909", - "0x7495f82F53F151DecF4Fe96eabae4f395AE3e2C8", - "0x63C913ae5E66825b21D83AAc5B58e859Ef2abBcB", - "0xdED2B63AFDC317E226D6a43780DE61d013C0acF8", - "0x3b2cBBE7fbC39846Cd15333B0e2F3FB89eEADE2b", - "0x46C4c4781fBCbD501e11eF8D87180c44026a3EC5", - "0x528935B9889780e8FFe3B3AbFb614d31718D9965", - "0x2c803dcA5549862f778cc821eF98c5b907b67cAf", - "0x8581dEf657860bF58A5b0d6d2D357B51B81655B6", - "0x3714E1078Cb6F67eF01442B4213012B16889926F", - "0xcA7c767854B1E7305dE07247AB85E30543d1d9c9", - "0x3b6413f38da132c1eE3E1BFe603925677e5CeAB8", - "0x85312D6a50928F3ffC7a192444601E6E04A428a2", - "0x99F06631425F01514e774Bf3208909fdeb5305b3", - "0x8c11E3Af9c1D8718C40c51D4Ff0958AFcF77fD71", - "0x8397bDf754Bc77E08eDAea82E88bDB6ee517a82d", - "0x6ffD435b88902388D5A9C65f2bCe6a75e1b14093", - "0x6386AaE7B2De8e9Fd895171f24F93D08545Fb4F6", - "0x32a534bB6afb78FCCE6Ba6aeB394B148E1C398e4", - "0x32477E70b660272f514978185cF14a0c6Fc68019", - "0xA9abe4e7cc5aAFf81Bc02F39D5f2b56a8094C9C3", - "0x62504bD6b512aaed4F6BE3D61B017eafDfd3964f", - "0xaC47e58de553Bb5eE2F30Fdb5fF8E6EdEBeba90d", - "0x99a5AAE28Eac0Bba6e1e2f6c1C0E5a87612DF6F3", - "0x648cf383a4fE746a0e507fBB522e6eCBFFc8EC25", - "0xfB563Cb2572c50480f0ff1A009DA54f1fC5D9AF3", - "0x28B791300024FBCE569aB163588CEFc7A5cdF546", - "0xA7195C71AA20481fA0c093E51323F8A4D3a2F5D2", - "0x5B3067C5e26fEC823d6fBf8F315e43632658D2F6", - "0x3491360D98559EC4146Fec5b9886d7f9f3762503", - "0x1A72992E9058DC444f0df356eB007F6aEbC6d867", - "0xa756B300E0B546dbaD3936d4B8fC9BFBdFe222e7", - "0x43C4fF14DAe2Fbb389Dd94498C3D610A0c69a89d", - "0x09cFB07583c610FEcC6780254D354cC286F496E6", - "0x73422A2722066C1750cb9fCbF79caBf1f08f9bF4", - "0xa0F6f7392283163E133BdafE35F8CD7d52520ED1", - "0x73459Ef0A33EA718211e91504E9CaeBEc0e6e667", - "0xF786b0717f045595c5A3904262B8EF649Bd666F2", - "0x5be138179A1516e24CB13EC34112a9C4dAB7b384", - "0x41a6ac7f4e4DBfFEB934f95F1Db58B68C76Dc4dF", - "0xb7b570Dc117b4946925bc39166418516FB011CbB", - "0xD1F9465e0AE9BAA60D4b015EA21D6C957F118896", - "0x5bcBABa9704F625bdb7EB1D8a3c305737916e6Ff", - "0x2FEf6742D30C81c518d7742D5c7Ae6723f64a79C", - "0xbba2379F5cc9A2f248C5Cf18aD72379AE2478F42", - "0xe379AD19528A01dE884A7CA6a7fE480Dd157c182", - "0x0a50F5F6301D747E2C67e26AaEDc38B9469a8db7", - "0xda4C20Bb77eEa27a47d27adaBF2071328d93a67a", - "0xcdc0b6ea0875E8d7519EdF7942625111685D6e9b", - "0x01C81C0E0cfd968A96CF08E3dC32f395A9dC075f", - "0x81db2dFD33b24419F0F715e331f678411Eb1ec30", - "0x7576ABd7d9465591B6A41d7d2B77E0c860A99878", - "0x54b55662901aF57B31fb6B52AF8175b652A5816e", - "0x08706C91ee7bf8016f8B33F3530FAc5478120fbD", - "0x7af2fe0509823Ae1Ed416adBf1f80D452116fcBd", - "0x62bC9BA8770F43011d3339ef30F50C30b404EDcb", - "0xc067e7F26f6e75BB6591Acf1FC50b6A5DcCf5835", - "0x6828459E3ACFbF6b787C4Ca752D242e6340D3018", - "0x2f843eC4bA0a0C14813EC00fc20F7BD87CC08F63", - "0xfe701B7793Fba9EB4E74F1AF5863D84f9d442091", - "0x4248C2aB690289C5AdceDF61981796bEaF9c3fd0", - "0x8fcf51B7562f2204518CBB8D678786Ce09b4106b", - "0x84807BF486A2Ec97Aa667c6dbC68f73D587bA150", - "0x933258BdDD49beeCa77F6D1889633c5429AF45EA", - "0x69e039d6010Bddf601271c2B51E126f987850c68", - "0x64d1D62cE19332B7D247b82116B50d6896A0A64f", - "0x6e28B3331C51A0DDb221ff833D499a2bC439D002", - "0x03F248f96c6ad8dD0087ff28f0FaB1e3205CB3cD", - "0x6396a2ACe5978c4fc9BA7AC3D2d3efb6B3055461", - "0x3720E6888aF0C9f5e3b3197F133924232adE531B", - "0xFA122D9eE820873027411FFbDa06853439B8Fef9", - "0xacf6adE9613A4d9b2801A8a490C4BBBA5B9f1ca9", - "0x9188714419B81c4c920F04876C19FfaE8f5FCC26", - "0x767504c377Dcd3985a55760E7c20599bfb21291D", - "0x7B36B298035e9925f46E7d3AD1cA16fe2bBC46C8", - "0xF6F8Bb3bf0b250D4B41dE6BC20FE57DF62c045E3", - "0x8Bf48230D4c10f011C4d1B73804E20436c239902", - "0x1a9836F6Dcc253332eE707Bd9808aB3d79d973D5", - "0xd2E538812961E563b82a13662a8C22841C1E459e", - "0x1124281Ab6bFBd169DCCACF43050527D5db09C6E", - "0x1F6e332E2a1bB6DE6104Ad18408239762770CCdf", - "0xACc43B5c8E2ED12D374e74cD91bC576fBcB9eE23", - "0xE9a4aB491942160Cc1F42449911D0E3dd5222436", - "0xAA56C1febDB4C9698CC09B6065eFE23443474114", - "0x9C1eE1dddBef7be7C96dF3BE5c63998f18f4C9cb", - "0x3fe7F445566A103Eb5D62C7890387F717fC06e83", - "0xdA294570098d1fe503BEAC1ebb63980bfC48f4d2", - "0x5430FFC48ff3A0D4E53574Bf89a7E9f75eBA524d", - "0x092121fd7700B97e68248f8763c4F3cdD955d5A0", - "0xb2754b61d93C0C0f00b0BE4fa7D2012B6Cd2B430", - "0x1874281e2386d85617eAb117259f2157334243db", - "0x1ECdd6dE99277d4228A9De50Ea73ba7e10D662e6", - "0x4815Ee939fE2efeC2f7bc415f0cE2282f6417fe9", - "0x83B285E802D76055169B1C5e3bF21702B85b89Cb", - "0xeA09CFC7239476AB030B649D5CEFdB786f0e7412", - "0xa8b4C7F8B3d91b324F815252da74884E68FB4C4C", - "0xf99C8873B972b4192e789F55AB954188D0d9A133", - "0x9b86449941d2f226A2211553f722C76c06d99bbb", - "0x1289c5314B7524491418FA5200EF2F966a3A923B", - "0x62b2b7D015d314e17A4b61553ddc6d205c0aE2c5", - "0x4bf4421cE8208b2e54C0b7e0B142192b47e69C4a", - "0xdBC35CCE779E29fB71799137f632b3f6F7f46573", - "0x6620B86Ef016e3B9F57A9dbbC5F114311D4F8D28", - "0x3cA99a967825891e2Ae4F5F05367AD7fA83f22a9", - "0x380fEFf90198bC240E1c47d4e3C0b51391DD2D63", - "0xEC57915526A7869b9b5539B31c90bc81a060A171", - "0xAB9b83a7F3416d921464aB018AE273fb4b284E86", - "0x3BA827c00bBCA8d1A6a81b0A590CE9C82C0AE317", - "0x1f05DcdB58A47a33d7bcE998FED28e2618B4f067", - "0x445726b59A24628295CF52e8DdFD3617b94e3932", - "0xA294a7623F538ef9C0497Fa8366a1D27ee46C3a7", - "0xA9D5E82719626496292be0575badB566551573D7", - "0x2b163Ade152Bff560206dAf5C89aE358e5044199", - "0x0EA41755EF53F8ECf448b0c19e05255F39E297eF", - "0x6803BC6e4Bd209537eB356FeB03c66bA4A383f1C", - "0x1EE840962f7E8414092A25a216ec94d06E4600d5", - "0x2e171682d2e7962fD7518DC2603F413cdF225F5D", - "0x00Dc3a8455daec001E26a5Cee271689D11bD98bB", - "0x318c012875477b1FA77bF1646df59B90272A0E26", - "0x44E8A67E451caFa616AE785940E9C4Cd574aE3b4", - "0xa5d6dA2B74f2615710436fA5c08007A37d97d701", - "0xd27bfA0BB69bd04cB869660b2EF97ACf0Ee3A707", - "0x0Ff324D42ACdCF93Ed7187CB9705B4609B074BB4", - "0x60305F33437a3956eDa0103d4e101021CfAcA6E8", - "0x01D82047C4A758b7D5c94F45fFAd6FE48F5D8EC0", - "0xf93d4eC4cADeC63A68FE13aD5BB1Abed44ebb81c", - "0x9388332Eb6465DA47363322De1964970A6a338eA", - "0x7511B3D9c01Da239e4f0c3D48E190949f68E2b03", - "0xa1091B710fB05BFaB226972aD70EE9ff0AA0dBf6", - "0xEDeEd75328937a17749C1a59772E2EC79eB51314", - "0xd6Fd705EE0B31a9300D3E2154bccE777270CBb6f", - "0xE65Ee5600293aEe2B70a60A48CD44A492eBb6974", - "0x93D87D3488B2774DBa828A53aF99b083587BcedF", - "0xDF42507b9656faeA7Deb14b8d409F84ed3542DfF", - "0x3B82B31bBE0DC3fE0f13D17f9a4d0455eF562e06", - "0xD83c4ed9aD3aeEad49b47AF8243d6DF6A56B9028", - "0x1d28255230A90F4C4Df8aB91fb9230C7aFEBee67", - "0xE39516C6eCB2C94E181Fb5b6822D70278fc76545", - "0x61C5CDcde2150A3b6c9CB27D14fc0AF0f42Af5C6", - "0xe0e4Bff7ED1Cd39Dfb4310eebf078187586526C8", - "0x3ED894DEdeEa378C0B8e9fE2DBb29e0525496c34", - "0xbb4d109c3dFe45fF30703BD2dE43dBa1c74cAEe6", - "0x4B1F140a82cF40A955D1e8cA2aA52E128b593722", - "0x85D5f9AA64848677b5972603a04a0fFb2F321038", - "0xF1150F15E5Dc578Ee6a960569bE240DB7a9efdcA", - "0x2b131FF66b1CA0348E1c20cdB1E5069485405f59", - "0x149396B7f07EECdc4024Fe2B58EeCDC07C356De1", - "0x2aD8aE2e1421B1d0a0823C0e8f71D40cEaC4F872", - "0xD13Cf36b646aDcaD473523F7B32bAa74F4F8F502", - "0xDfb0348F0d8200Ee2eb50aa4755F690dC3165B52", - "0x5fb92b742BF451Cf55dbC51781A621bE9Fd225b4", - "0x501C2CAe71D9568da1972b03f27eD2D527D20aC1", - "0xe32b9ED080d3051Ab4F291d9F65D7528d139ffB4", - "0x5750D70f57658b8A28a8ae659c3f8020B9D4117f", - "0xEAD67D6463cc332507c4B321B022E84e5FbCAC97", - "0x44af58E9ee170bff3f09B224110620DCE521Dbf7", - "0x2aA6b66cE9432a67953D1F37533CF0B05A5F6e08", - "0x03F22215ebbD72FBB98b15d39FCDbbe6BC7b9A8a", - "0xC7f56d386D42151018C6dd0AD08efDf6240A618e", - "0x35E8171BB85a471C8e6B379C19515006dABFf236", - "0x4534b2B160217849776B1885e082b0e39Ec7dF08", - "0xa0Fa13F542c1f45fAcc537F8Ba8db3c4F91a4fD9", - "0xC9478772653fC70f52e60c529Aa9de99f4ef95e2", - "0xB005fc539BBF699Cf72af82343e0da5dA5fcffDe", - "0x70B31325eC1EEd59e2FEfC4076F08C06C913940a", - "0x041Da975e077596370DCFd6E67CF546e2C1bedCf", - "0x1Ec583d32ce166A93B4E87f35c1bbf99017f4029", - "0xF848C171f708C45B28Ab299Cd58BD239B9836939", - "0x9b3E84714e7b8501648B50e2fC002f8F3dEC5Ad5", - "0xB489400A5237A7cdB38DA63AB3EeD973a968529D", - "0x9bf6c52D4daC02590D6e621F5edDCaa716eAA23a", - "0x352D68686d21237582566c6b95DD7edbF04314dA", - "0xa55b524DbdD2D7080FaaDAe7807CbA904eF8A827", - "0xba0BD4FE75d076Ef758131BCc15f435C45ECb227", - "0x491d927Fa6c49460446346cc93e7B5E75B0B8c7B", - "0x9c4b88fBE06b6B1E461d9D294ecB7FB0c841dB4f", - "0xedF5025c84231784293970a5b8A1aC9D29f9b758", - "0x5e334FC2eEc978478E84d17446d842bBd8C5Af7D", - "0x3452c9A1a2f670FBB4eB6bdf91C1eF4E752569aF", - "0x6a4Bf9162F59339162530D2fd576a2604212E9f9", - "0x04Cd750e3e1D4BFc62c4e4A972ea3580a68a51DF", - "0x5458210BF50ED1864639F45B5AE7Df76Be2177e4", - "0x36d481b3c6ceca782b1FE72a80D704dA06E6c063", - "0x56FED5Bb34d8ecAe854A58502beFe1E1FEd679c0", - "0x49a3Aab8EBe80725646937b9E9f6f8b4e9867bfe", - "0xd99591A00a7Ad4B5c1Bc560eDD219f7749aCf2D2", - "0xbA208F8Ba2fa377dfa9baE58A561D503C3F4d96C", - "0x84d0F74d21a89F86b67e9a38d8559d0b4e10F12d", - "0xe55C38718aaA0e777F7c6057070a89134418c67F", - "0x8Ef63b525fceF7f8662D98F77f5C9A86ae7dFE09", - "0xAaEe61B58DC32E7331104F7A51d2A3220edf4261", - "0x2D7cb9d0F2cF122fD8768c9c7dc30A611243e00D", - "0x3EBbdD78edB895d8946181E626eaDb49d58E62f8", - "0x3DF5d15722E8bF01AF9Fd19c2e2e5Fd3934357F6", - "0x82767B4b962A2C8d0F60df6e4797D77aFb108904", - "0x4668B69ec8a429597Da7530a8e328f6CFB96D71F", - "0x106BF19D7f44dFd1e4D41b0890E568Ad7Fb5e511", - "0xefCB1b77e1f14F6B8282535B96Ce61B6eA99F1a8", - "0x53811bEa240EDCCDD922dB6733E5F13bCDE1a6eD", - "0x6F6830BA0571887fE2B5Ce0bE27814Fffc4029DA", - "0x56e08bABb8bf928bD8571D2a2a78235ae57AE5Bd", - "0x0de4348c28c4e9eF19d5Faf4024Ef6653c326ec8", - "0xA7eeA325c4131396b5ec63a175C9d00bD45a5098", - "0xD3d20F2D89922311F87Cd954691102f475833770", - "0x0d0C4D8bc5029E8420E1Fab78D1373eA175F4bAE", - "0xB8cc0AE73c5Ee88Ec33Bde0e3795F0667Bd46D19", - "0x1012e83Da77E283910D00E9Dc1Af5B103d62db12", - "0x0ae43D62AB68d0028B54eb67457B7c013d4bf6D1", - "0xf1B1Ac4CC89b151DE017e06ed7D82436505a6056", - "0x78b127298fFa031F41D1a7b55d1cD6Bf02A272A6", - "0xae4EEf02E0eE91061115fdF580d725D1544a2922", - "0x63B0A55c72ebebeaA528FE28E7971C6195C7469b", - "0x94C4b962B5Ad65Aa670FEC3ae6D6c7b27f8a11F0", - "0x3bE960F410100916b49C41bb8fC52eB5511ef307", - "0xBdee80688Ca00e6f89dAF1B8CC76Af0D0cE9EA45", - "0xE0Fab13e3A5fc6d7BEcd6fbc93E36250375491c6", - "0x2Cf638A9465B34408847C69AEa170F2d2f5E3153", - "0x9AEF7C447F6BC8D010B22afF52d5b67785ED942C", - "0xDBB3028717E692ef4C002cC7019AE7192bF582C6", - "0xE1518a5ae4C762a0Dd7773B987F6BD8bd7cA155c", - "0x1623273D676D1eA1C4Cb5A85F2CA5f1c06C2de93", - "0x612b8E8762BF14F445467d023F9f04c0C22d3DBB", - "0x3B2dADFAb8Fc9859d2aaa9Ad09Cafa2B179f67BC", - "0xFE2fb8587760c8d5960CB7A5BA2f2299EdF10506", - "0xC29dac18Bf481C1615caEbea293B84C15B383828", - "0x1aDcF07389b1F6605C44a7683c50A5243829A92C", - "0xB92246136f049607423B4C986302D407Aef91A17", - "0xCE1CB8BB004D6f3b9f8166E3fb90617C5b723A70", - "0x97F16b00d436FcD49D5911e68002F1cd4d5e47c5", - "0x710969f07f9B94428a612e26dCdF7728524B01D1", - "0x7AFDd1bA1145e583FBa788Cc21dA686E736A311C", - "0x69DA58fFf827b1Cf91677b72B8fC515f61784762", - "0x205043ddfa3F742bcca920332bA1e6970701cfcE", - "0x7569Ff833FF6C5fDbb58934862A08281828673Ea", - "0xDe5634b9199Ca0Ca036Fec876FC249043dc98fb5", - "0x750f53f8Eb3024Bd63913480a0b47F3E33Fefbaf", - "0xf89FbC24FE728b699a6c7D05605e2a4b944c6E39", - "0x798031C6b05A37C4891E6928B1C85fcF153f5D70", - "0x59D274BA41a55827062E2E4aD115D8FF758F391b", - "0xe51F6b8b148bEE49C8A7ba87cE088d7081EF6912", - "0xF5EDFCDE8B5e43EcfE382Fd5855612b3bf611224", - "0xCB966829e34f1b376715471398ba3E1fa50681C2", - "0x5fc026Ab7F7C6ac62c62e4382F7FF3d37e2C2a75", - "0x84dE7a37F1d1c05aaa1502f87e4330adFC9CD32B", - "0x2e060833573A11E2A06Ca467F12DC99AA04FDde1", - "0x5e7a0aFc7e3C32f844114d3Cb375ab0C456A57Ae", - "0x5a0a4e95f30FAcd178c396199a9f37b6c9ef6b81", - "0x3e763998E3c70B15347D68dC93a9CA021385675d", - "0x64D196E9Cc62eC70Ca0379DfC38CCBff344dD38B", - "0xDa10F11ba76503a2d9E3F29d4b92b6Dc3417bbeE", - "0x33606226e70B96cF2a07E85401A70661adc3D019", - "0xBEa645Da9c7f6De1f7E287759CcBFd5ab9f57C08", - "0x4f555e32E41c6f646ca72A93130fD24be10A8016", - "0xf6f40CeD4Cb39ed9Eb2c2857b8cD0CCc2dDAC241", - "0xBfb12c30a347d06778Fe9DD582C33c6deDB373ef", - "0x02802e1aE8da338C8a5d220D857D27c49Fa86A81", - "0x5761774922b95084454c8720D647250bAD207EA2", - "0xB46782AB70619B1BC39e1342E9E77921674e4A33", - "0xeC8804867a84cFB397debAD4e1BfFb1F015bd7E1", - "0x8842F97d36913C09d640EB0e187260429E87d78A", - "0x01ebce016681D076667BDb823EBE1f76830DA6Fa", - "0x8a146531630850fE1d158c922bDA620BDDF12766", - "0xee1cd2CD8906A5e03b879FD1a05E9D08e592125C", - "0x3d2B23962EBCc882f9f65452658BBBa9Fa72d170", - "0x35e45b6421c01a131FA42A00f34091C3007a952f", - "0xb14d56ce29dADE0316EA5Df06D491636a7c4F3C8", - "0x4C71e10fBb6feBB14125EF5bF100A2104F5c1Ad2", - "0x58C4f03a954e4CbB1b8E204a881a8e9A99d015Dd", - "0x8234AC0EeE3B4b77EF94E4cf2681107d19025562", - "0x193Db18A5EF9a0320b7374C1fE8Af976235f3211", - "0x469F25C297b2E84898cfB1CE596f2553285f3E9b", - "0xdBBe2bb7be24319808E01DDf1dDD9276Ad2556F3", - "0xaeE5CB72Da840cF679ebec780e31295b6BfC746D", - "0x9ee30B8D06017d579752c19FEEe719D2aaf59265", - "0x7ECe13F2fEE7f2fF9Cfb56b1F6d1EF2A787D68D7", - "0xDA5d647aa9cFA75167D0A9F299fDf01beb9Bdd1f", - "0x23c043a293b302E146Aa9fcf9a9CA937b895dd56", - "0xFC19EdDC148e92a59C67d1DED4bd7f27aFeA0194", - "0x92Ff028954F615c00aC555f2E852D0E1b85Eb5b3", - "0x79e21f66b9813f7922fd56E14051bBbA83EEf96D", - "0x1e2CC37406F868b3d481170248e8aB2fb0181644", - "0x291f1593C2bA68974bC6E0AE715b52ee313813A6", - "0x54BDb287edd2b9FBa7cbBa098783bd14B19DB93d", - "0x6fE4aceD57AE0b50D14229F3d40617C8b7d2F2E1", - "0x2A9C628F9B1b06a881407AE457Ffb0E51FB7E752", - "0xbe61858c817A6A17774F8079A5Fbf9D1896f3736", - "0x8F25818ff64Be1abee91ABca70c306B33b602062", - "0x942c9a79866EbF5D6F24e6b308820541dA6709c6", - "0x645C22593c232Ae78a7eCbaC93b38cbaC535ef12", - "0xFe202706E36F31aFBaf4b4543C2A8bBa4ddB2deE", - "0x755CE5F31617deD6e41D15Cc51D8FF642CE10e9d", - "0xCA9c7E1c9BB058d92f1ace6A389857eEBe7b1368", - "0x906f186f2398949FF16183D1Ccd47796d75204C1", - "0x5Ab8E8a5Ac6Ee75BDf3ae1E9Bb5E354232768053", - "0x49Caf2309CBaFDFA4AB28D11aE18C3Ec9b1Cdde0", - "0xcbebeF2DBAFeC16529277bE933d60CB0524aDe9A", - "0x0d8C573e3938713c02cCC77D569eB0E5bc4Bb20d", - "0x3029087FDE42070F36e779467d0780B8a587AFC6", - "0x264a78Ac40c8F6EB8B3f163DC6E0CC5A1FE9A100", - "0x9390537C031B3f53c5982A8F40821602fFE3B28e", - "0xf9DF78D070F24c644D35a56a8F2971b590084e9B", - "0x75576e0865f7e517DFe6917C03d70037EEce3Ba8", - "0x827C4243634e99B146c9FC73Cf703af5e5D183CC", - "0x8D32b6272a22deFe432100A57D952f7e8533692b", - "0x6c3C301bB3AF46c86205844c7Ad9eF8bD6593BaF", - "0x9f4d78366F3D67AbC8A69B44823e4ecB8703ebD3", - "0xFe9B6965c27f24F90180054175e07D4f9b3AaB8C", - "0x927154b2A5B19638c2A452e8F74e0B496C2125B7", - "0x6A1Dc2f320AB982637Dc4b67de1C0cFc4Ad988B1", - "0x07e6ae8F553DC77B8b372e4d20dAb797475E6119", - "0xe6FF466b9E9259DCd31A9847DC11872d6b066f20", - "0x001d0bf28a03dbd38a92cA4ACC6c4c7008C952D8", - "0xe052b423cbc4e0103Ecc7FAFB0ec6C0A911Df40C", - "0x14Ce0d3074E93f2761810fc64183A2bA9fC33481", - "0x3a4fa1bFC8EBb0C958C4CB6c0B72D53C3aCDEce7", - "0x7C11a14D2010e3a0A93b6291449ccE10b2e38f66", - "0x023aB8e20a4682d315Daef4c91DB96bD77934D66", - "0xD9Af06CA2eB971f818f0c41D40e5B8f89830d7CC", - "0xFe407097E296C521c2BEAA5e4E61A9b91bbe0D61", - "0x6FB2775c1424B5B10bD810bE11f68274264a7E48", - "0x03FfdB95D3eE7F7b796069867E55CF6fdB65472C", - "0xF3E3F3032Ef4765DD523D5eA2CE23c36c24dCf36", - "0x3b1b211B3FCe0D1EE9f5e1aFeecadeB98378d0b0", - "0xc0A736fc46578a9D0CB5595f19035DA934755caC", - "0x98B5532FF6201D9bB679EEE90cC34C10137998F4", - "0x0952222D50584a63Fbe38D5B4da94b06Db102d46", - "0x589fFAeAB4A99275660a9fa4274cb58F7329f4Df", - "0x9050c755691E9089E08aDee29462BaB6b338def0", - "0x8074Ed7AeA09a7F371aF44daBD4De6C42839929B", - "0x7A921b0365B4571beE00ba5A1DbB422B11f13F9e", - "0xb119efDaBF71E3B38CDFA393Af50e972a1149e89", - "0x85D38bA18634f64bdA3BeB3518DdF41796ca2024", - "0x283A2549b640E56901A8Ced2Ec5277A32CfE6425", - "0x80b744588fd2feA8ef14386bd953493A097eadb0", - "0xD77ee49BAD638Da26775779e830339dc3F61edE1", - "0xEb3aeeC75F1C6F0E3525c0e9d7ebD5ed37A2B8FB", - "0xa0F39111d7D1124ACa553e53883ea4d991a80675", - "0x083E952464B4a8e988ABE39868a652f9C2AD050e", - "0xfB17d5CD85854B6Bee89e714591DE521F3169dE5", - "0xA38F91AEAf08a8E8a9e488DCED678099d688900e", - "0xd1F4cF428835a775916Dd767e11553b5649EAF87", - "0xD0FE971732E547059eA6009c79a2d6155bdb41a4", - "0x7DA220633dFaA2dD4eF3e3B222e836a2E23A496b", - "0x0442F45807CF2Fe3a3dAEd99762cdD9cEfF853B8", - "0x82e18803b9369C8210390F70cd0237480f7EB1EC", - "0x3A51C9CFC601E6192d24A33dffF394Ee8DaaA144", - "0xe13a47718B9bfD9F74008068787827af3E5931B5", - "0xB1bD394C25F653b71F56F66DF4a54653dcBcacA2", - "0xB04f18452bb410eB664C03498a481a3c04D6EF76", - "0xF925cC2cbea0b2B30aef6C29ec0c77d7cb6FcB6f", - "0x2346FEF37a4F36753594916bE2aBb15d2566C4ca", - "0xF6C25Db73423ACB7249E1fCC4ea5E3f7f1eAb4Df", - "0xadD2975595fD251587844D9E0FBc8A31341D2a27", - "0x28aa4F9ffe21365473B64C161b566C3CdeAD0108", - "0xDC20107902Aa222658a135E6f61F905f47998387", - "0xD638dC334f3A9b6692F7Aa71a32949F7da12F9D4", - "0x5f4d787692b0ab325DF37E0b999C452d4561A8e6", - "0xA4264AA379aD275aFaD55b77d22dE421bF425280", - "0xe93F324C0Dd0f9C558B9A4652927E5D06cBe85b3", - "0x69b8A24edc9053b3D007710E7B986dF40a0BC7eb", - "0x838998F5EC821aBf63F17C72025E5d2E277344F5", - "0x8a806Bc475331F20022D897E4f9DC066Bf0324c8", - "0x1B647EA4993946E00DBc9915F76d88212b51A2f8", - "0xfD20125Df89e8Ffe237b5bcA38acdf3cC54Ad5b4", - "0x54B325783095132A996027a622a5B4Cadc8bE876", - "0xCE5B6d4F24b06F9c93c42653695A26AB88f1B951", - "0xf9AEb52bB4eF74E1987dd295E4Df326d41D0d0fF", - "0x9100FfdF05ea6898002Caf643d3BD0433ABC1d15", - "0x8Ee25bb8D6777c21cEffdeA20583856c620DCae1", - "0x9C9553CD0c8673651A36e5E11C4989bb629D4067", - "0xe0647674B89BF40247cc6D7109F3C256949EbC64", - "0xb616d066fb1aB1384D47A5a70Ce00af515445A6d", - "0xa4F967B84Ff09E0bd4368EF934499a6a3E4BEE22", - "0x0C13f7c69f1b3D05C0a007E29f03b4d948b0859d", - "0x7aAF6B8506F030f0DaC415d03C87E89Dc891EEed", - "0xEA2f509cABc86E09E8b807258D3f9A5f72F0da11", - "0xcD7d70Bb7034102Ee0C828FBB83776E0137DeA8D", - "0x21f59001e8543fbd84fCACAdE38B1A06274F0cA3", - "0xfE81028185B4A2465751F181bD8D2Fd318d460B2", - "0x2d9039cBe219420ab16d31BcB7a37B395f0cE394", - "0xB7deB9804764294d7fb5296BB81519F340247C03", - "0x37900736d4BAA4fC9930C1a89352391228628C64", - "0x64e4fc6E6e153786C129b70c4477D69c69D870a8", - "0xbEBCCD57f93b873b017457d1670E77E5F96767aA", - "0x7497B333895C64A56924B293e68a6468963f2A7e", - "0x30044b7C080A6e456386d5b9c370Dc7c259AE2cd", - "0xA05e21550eB86462388783047dA318D2de20BBda", - "0x16Ce1B15ed1278921d7Cae34Bf60a81227CFC295", - "0xC166b67C8D94C1c1F9AfDe897E8CA5d05Cd2385c", - "0x2B01501C2E2e3200E68F1FdC557a10C82Bf05626", - "0xfA9413b9F08252588B4040f888c5F758ED6D10A7", - "0xF116569b3f888D639372a5485685A6D8EE28A593", - "0xA51b7c8d109B0aeE51F8C64165Cc170354b9323B", - "0x2354910D293E778AADf39B16deAadabB5B40D66b", - "0xaae31EFb56aF2d4DA4f8B98aEE5F269e03D1fce8", - "0x5A09EdFCC6f4aa80EF9c996521B1a33650bf4c81", - "0x094791fAAAD2a4a10f27838bC354C4D7d0BD8e75", - "0x27667Ac10cA637448ed6cdAe46ebB8347bfA6b34", - "0x3bce96AB4cABd0b8514E42A84A074403413863a8", - "0x24A7e2F78c3AC4181996e415f7dBea1A71994952", - "0x0422211c02fef3F623B1dA67A116482B029D8BCa", - "0xB4773a58BFa5eB5f1029334D5dbde2F12Cd35D42", - "0x5bB872d51BA73847DCFc5031fEe0af568d2d83E3", - "0x8fCC6548DE7c4A1e5F5c196dE1b62c423E61cdaf", - "0x9D99E6102562F3465988402831b3843A923f3aA6", - "0x625Ec8189A41132EB43166929f9c8B20014f74cB", - "0x1CF8423299241F155716eb19D0ca65BEA4353F03", - "0x4e53433495619613251ed5d9360d205A3B87d047", - "0x9fD28576964041Bd73ea68F2F09EE587c858d291", - "0x13dbD75AefB4ebddea327071257948edbA2Da6E1", - "0x9B3fe4109F9Ee66F723CbEb353CEb365Ae666Ac2", - "0xe2eFa7C8A0769e91af174870DbDdC2D2500c08a3", - "0xd35E24699cb637207c7486A570039d30FAFa824C", - "0xa7DbeE7f9CCDd8c91B75067CD31cF0b6d594fbfb", - "0x1D9c68cfA5fDFb6C585bF01f715d359f3039b63A", - "0x98A529e3488c2d44566881FEAB335B17B1c3b430", - "0xe827C9485d095a82f427a4D1a54e781663DA2de0", - "0xCE91783D36925bCc121D0C63376A248a2851982A", - "0x59D2e0a2be0DB463c36038A5fd2535F58Ab3B38F", - "0x1393D846210262Af69a29587AEd52C24B0Ae6223", - "0xc4C6E038C1A126C57F540A4c5d57f8B02d05fda0", - "0x908483225afD362fEbE58F032E1CcB82002401bC", - "0x26563EEdB269448352Ba0663f2EE554FBd389296", - "0x7FFDd3AedBE63FD45570e81F7F75Fbe99463470c", - "0x50f527A8f3898BC4b26f5b032EE60C5fC906A2C9", - "0x40Ee2aFBFBc4286b25e2A194920C7d9591eef9Dc", - "0x55af604E03BEb3457DD754d2F630eedDa6A1fCef", - "0x1be8f34Da50d68a5452538Ce6c53B43DAec148be", - "0xF06a70b9248072f7F17492A962557447E0001152", - "0x3374cb587bF1bb914946B692eF7AE80f8C344091", - "0x153B4F4f2c6c284f844A53eB2e097D23B7bfa4Dd", - "0x911A182A475C0b54B6A861c77CB5A8A1e69b482b", - "0xb9806d3A651eca707DCeE20E9E82310754878fC4", - "0xb6888B2f78939aF6515D82A654C0e9a8763b8722", - "0xa0F94a872D5736471D50a14cD65C81FE5bf27C79", - "0xAA916C42e129E6344acd5c6aa5E809F19A8F765C", - "0x03f65d114DD493B129Eab94B8eD8190a464d3d5E", - "0x44A67412F3B7298dB589e456253172A4fd2EE541", - "0x0faA92aB469Db5D268fD37C2F1F5E39F72B0E3B3", - "0x9FAA6a2CD5385Fc277498254e535EB617ffD0C63", - "0x7f11BEB8887F76d2493D6C18498c27379473C85B", - "0x15E20bF6F80f6CA69Cdc6F03483e2d392860eed3", - "0xA1a249a46D9334c372540a1919e71210f1197D5a", - "0x3F7d5f190dBD3d0f68fb434efdb3267c02E6720F", - "0x1CC6722C1cA27B1B2d3D89Ee5854b4F76FcE829a", - "0xe0D01EFEe7A9740F8e702F086dd4FcaE87926Abf", - "0x76937f60b76DB3B5330B2d257138F994dC160Dc0", - "0x2eb7D6C8e738d1c9a35E7a4AB43D50418a637b7f", - "0xF5A945F4FD8633B4573Eecc61D5307B267831C90", - "0x174E070920007350fA0193E0321f9bBD3984949e", - "0x1cC3a9B765199Bb1Bea3cbA45b4A203C55ec4E1a", - "0x3A92b4BF4F6528dD12F8b0fEb4E91768F65Be781", - "0x81a9769Bbac5E53F665c0deB4EACBB0094503810", - "0x9Cd75894548a8969E924F0B3aF6bF8dC27612b71", - "0xf147E390aaBaa560C34d44aa84510858c476a677", - "0xA6A2AD1320Cec227f24f404fcCC120B71b41757d", - "0xB1e22281E1BC8Ab83Da1CB138e24aCB004B5a4ca", - "0x12F20fb5b6047F129562c97b58fb2a78cd80b765", - "0xa3615B094D2aA6D93EABb694495Cd13Efeb9916b", - "0xd56EE5Ba5A52e15f309108BDd6247C69B4F624C2", - "0x6C0d470D97ef1F230EEF10A5c848AFCc8261ed67", - "0x1Ec9Ce39b453FF37bf21ad441Fc4060EFA90c4D4", - "0x439eC0fe84620c097501d3eB6079B37204e446c9", - "0x867c55Acd5A4691d9336B26bb15c4dbEdE5C188d", - "0x58Ef5039E1c799c4C2A454faE28a2dEE21916E44", - "0x3D6991085Ab1ae3926cB96f25684C40a364B6856", - "0xd62C7913aa2B190Ba54aF680C92665c88D6F89B4", - "0x4b7292679D439CCd83B972A5fF848b47391AFfCA", - "0x94382cF5d7F6eb2dE5E5F41553086F08b14e11be", - "0xEcA88938aBfF4F6329fE99Cf67709B392DD0F7cb", - "0x901Ab2d7EBe247f2247DF9cC090f81714920E0ec", - "0x1301910A3508E3D5b5549D87073F0d1abf547991", - "0x6E2Ce0D1f8Ab9C82Be2D0beC89a29fD9AF9837c9", - "0x29DFA3B040A65ee3fC0DA515343c18cdD49c635f", - "0xaDE38Ad7cD00C20cDf60Afdd679BE32F87b7D802", - "0x5FE3971e91a40C7Db540BE68d0aF7e154f1d4f54", - "0x4b2651d278966402a6dC3F20F60b590Dd93a81E5", - "0x5Aec24AcA614bf525D26087AE41ee3f9c7B0eBEe", - "0xb9C0aBA138B98656FfEa4309bfE2881b0b7c1d96", - "0x02BebA6E034d547c685Fd496dD1EA783C355fe7D", - "0x2DF32Ef622c427Ad9A3dF6bA76b992622e2E381f", - "0x1F87eE1e1e73fcF1dAede163B3F40EEEd9DB7ebE", - "0xF9026C6FFb136cFb190b95BF617567a23452D823", - "0xA8E156Ec480a503410f94383ff6c01B074c10e53", - "0x7C6b59F52578a1bea4F8D750c5B4BB044669B5Cb", - "0x9CDb231Cd70B7522c2b43ad18240649f9599F4BE", - "0xA628114D249Ff3De888C9076A2cE370175e50617", - "0x9086FfdA7e3E34Eee95dF281D61473Bbc2d18f88", - "0xeda9CF63AEa93301b1AdcaD3C0013Fe39CaB8cd3", - "0x8322961C710571556Ec8bC91554f675CD1828E85", - "0x8EDAf13697bcc248a428DF216B42610f9AF5dF54", - "0xfc2098B31f7cd6e520836b699cDB98CB256bEEF4" + "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": [ - "24088642601929700000000", - "4013884798687270000000", - "1427271995945020000000", - "4923400486461240000000", - "565699216017207000000", - "1050972850795830000000000", - "22860265809159500000000", - "871520509864269000000", - "4413593223200000000000", - "6602315280662690000000", - "1611304315837500000000", - "17723329336693700000000", - "11543880254463600000000", - "20488947641474300000000", - "105856431752225000000", - "93551344110000000000000", - "114418266099801000000000", - "6339267167826990000000", - "404546352908108000000", - "46781279652054600000000", - "20294576261006200000000", - "10904169762141400000000", - "1124372328441090000000", - "6525229076767460000000", - "3174350247238620000000", - "3785674422987690000000", - "31824786145484300000000", - "59375060732274000000000", - "44039131016679300000000", + "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", - "56624150366404000000000", - "1527050294915700000000000", - "575814141931471000000000", - "164658078624408000000000", - "724046427866970000000000", - "59547401319730400000000", - "784563374179046000000000", - "1718985616188750000000000", - "1346393890771150000000000", - "77088072125010300000000", - "252984577171841000000", - "28173702954232800000000", - "0", + "1661134296992660000000000", + "49725625571970900000000", + "2145135710611280000000", + "35123622593951300000000", + "8578381843859420000000", + "2275980528732120000000", + "10403491363462100000", + "16686996811056400000000", "12672529542113000000000", - "2750992456034050000000", - "4143478737845200000000", - "928746209917108000000000", - "142788118763215000000000", - "35621236542900500000000", - "788865388170811000000", - "108773147542576000000000", - "2049134102901940000000", - "41377436467500000000000", - "82554674162924200000000", - "5758061256154080000000", - "110645137542310000000000", - "5336269249337900000000", - "4930973932668980000000", - "0", - "11718234684533300000000", - "3302034629882290000000", - "53014629182909200000000", - "1801573361400320000000", - "36275830366292800000", + "27251318918576300000000000", + "502954593146370000000", + "10857607824309700000000", + "3750373554422920000000", + "13176166038595100000000", + "33712196075675700000000", + "425732796288115000000000", "680431094033721000000", - "4850615368811720000000", - "108078781004553000000000", - "34294254265343800000000", - "30773222452559000000000", - "15015222843787300000000", - "238986748003075000000000", - "56538316120943200", - "8933948820673910000000", - "4168563029289580000000", - "499585386803338000000000", - "3407075238484640000000000", - "372337653222345000000000", - "421478143541406000000", - "463089461141888000000000", - "336204948097149000000000", - "1905744860928130000000", - "3282493148280520000000", - "89301806769450400000000", - "1933297571040840000", - "33248248720684300000000", - "335961632636961000000", - "18091558611882900000000", - "0", - "39214410314612400000000", - "42835777950124000000000", - "709633092784286000000000", - "299242003202194000000000", - "46604768740956200000000", - "8616444660294470000000", - "112527329541203000000000", - "10529286989025500000000", - "2159077793577170000000", - "74334591334499300000000", - "151770625676190000000000", - "139999348243681000000", "17932576378856000000000", - "120777382121351000000", - "2531880102781820000000", - "47611604406505000000000", - "18530798502515300000000", - "1684666010080800000000", - "334407398491810000000", - "41677048966090600000000", - "8749571843959080000000", - "4640232073794480000000", - "55087034315589200000000", - "2758495764500000000000", - "84492535034905000000000", - "34633772175548800000000", - "444699737307917000000", - "0", - "6747914575116700000000", - "9980331715136730000000", - "109834243331086000000000", - "5852812942060680000000", - "2727048912784700000000", - "328314505789069000000", - "20748302140643900000000", - "18923921389153700000000", - "15762547989779700000000", - "0", - "10126327611648900000000", - "31216560743583900000000", - "275621478660415000000000", - "264681186490461000000", - "38396514295188100000000", - "982982538857953000000", - "28546493350814100000000", - "337121961632892000000000", - "4255311026900600000000", - "8285886952310630000000", + "124290376395421000000000", + "1247285812332510000000000", + "99417919518572700000000", + "203820306954279000000", + "5191547821152380000000", + "1915765998377550000000", + "91804687990722400000000", + "7401598730507370000000", + "189958632420859000000000", + "799399799598261000000", + "656644721180039000000000", + "939156864774516000000", + "18091558611882900000000", + "18442529688075700000000", + "3454828367324260000000", + "50817677020074500000000", + "2677631265999160000000000", + "474918798654353000000000", + "18361508301700700000000", + "10569012153755200000000", + "12525748785796700000000", "61238227689914300000000", - "32978534287337900000000", - "1400162681606850000000", - "286298464815918000000000", - "473372782313472000000000", - "6701633275329690000000", - "1594128471902180000000", - "292471746502122000000000", - "118631599727074000000000", - "41020528194136100000000", - "345603372059020000000000", + "1485223775584830000000", "41615617300671800000000", - "3566619535925950000000000", - "43189689204002200000000", - "0", - "1123982045720600000000", - "12593159980195300000000", - "82969291005597900000000", - "95219158090209200000000", - "20244589158231800000000", - "0", - "16893949873324300000000", - "0", - "116779415084789000000000", - "0", - "22369245371495900000000", - "2228611303169700", - "5877520060747220000000", - "20328692385145800000000", - "15447576281200000000000", - "2909402099164420000000000", - "24457226639740700000000", + "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", - "0", - "0", - "3322179143610120000000", - "9071072232969590000000", - "1216330969830820000000", - "1094862410343480000000", - "21900927562079400000000", - "74362357972469400000000", - "76542949114228800000000", - "60611885878321700000000", - "4413593223200000000000", - "148866467086687000000000", - "47927810848266600000000", - "14266162256970300000000", - "108906684858919000000000", - "101639356292888000000000", - "34149769390436600000000", - "2617952510110660000000000", - "1321435549416140000000", - "58017875634191700000000", - "22730626677199200000000", - "3107774810270030000000", - "880988610495138000000", - "263281414414537000000", - "3102222413754190000000000", - "9896487157363650000000", - "766342772799194000000", - "17600787286968600000000", - "0", - "5805699003800880000000", - "1103398305800000000000", - "140352929892287000000000", - "33712196075675700000000", - "4485461892041390000000", - "0", - "19030391638045500000000", - "74577081407035400000", - "2275980528732120000000", - "1271244273955960000000000", - "88384916250289100000000", - "52711726085449700000", - "2478684148059030000000000", - "41253564968615000000000", - "8670484616998950000000", - "753391214112001000000", - "1851994466344500000000", - "0", - "11628523031871100000000", - "87780478199674400000000", - "25062255755700000000000", - "0", - "1781842931547050000000", - "786573524228323000000", - "29598778235304100000000", - "14968215057600000000000", - "1655059432151480000000", - "3168238195527900000000", - "40202561246068400000000", - "131918420852024000000000", - "7247259019346760000000", - "107737719409414000000000", - "73682183413849700000000", - "2762625840152820000000", - "67786550452785500000000", - "33209161867610300000000", + "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", - "96197074328258700000000", - "103202771208286000000000", - "246815719201231000000000", - "9149475147496690000000", - "3907878384912320000000000", - "6950390109535120000000", - "63164899779223400000000", - "41302593865209600000000", - "8621056337215880000000", - "124054114943046000000000", - "2785205436063240000000", - "11800539239200300000000", - "120070784230829000000000", - "237777577292209000000000", + "868008146837614000000000", "14425510705819500000000", - "51830254294903700000000", - "8578381843859420000000", - "177243033492143000000000", - "4085083236793990000000000", - "1391680038032650000000", - "135543136015007000000000", - "11226161293200000000000", - "1241067931330440000000", - "19217317107076200000000", - "177036355080736000000000", - "8399869738398540000000", - "111056930117748000000000", - "0", - "402969609545369000000000", - "1149457875219370000000", - "0", - "5014880734125350000000", - "3454828367324260000000", - "6367067079347330000", - "4238195636619460000000", - "29750376820132500000000", - "5151166185422080000000", - "2909402099164420000000000", - "63511906375011100000000", - "13379079542521000000000", - "42885127545212000000000", - "3750373554422920000000", - "177440486313685000000000", - "3628060891875000000000000", - "8861801571017660000000", - "11608931927484600000000", - "6294087852604680000000", - "0", - "139880281183807000000000", - "108792549066083000000000", - "168900880485534000000000", - "146552123729600000000000", - "175173431817848000000", - "756879199814000000000000", - "16668101089553200000000", - "11423714762203500000000", - "37743394689708900000000", - "2709507925774800000000", - "128047223560434000", - "5515801142528230000000", - "42508492966918100000000", - "8762155339739080000000", - "172954072916277000000000", - "9932915823009380000000", - "25970759532457600000000", - "216311546925898000000000", - "0", - "10602485665800000000", - "30343475075277400000000", - "103909360425662000000000", - "2033756172292920000000", - "996666715837819000000000", - "87393343794551000000000", - "116998114719940000000", - "115400699114051000000000", - "5735580498348410000000", - "2485705731978240000000", - "87330021557646900000", - "23771902874190100000000", - "4269934428752100000000", - "6733936991088720000", - "189001942601394000000000", - "23863111755700700000000", - "2042959082185950000000", - "118283810654331000000000", - "13484878430270300000", - "2974847393597380000000", - "63247897000361400000000", - "20100573451382800000000", - "29380826968074400000000", - "21308537281240500000000", - "3167378718673160000000", + "7251475167310520000000", + "139348527974419000000000", + "276469909500170000000", + "262251850968558000000000", + "9273068414413280000000", + "2583100386752820000000", + "11171907846225000000000", "97309325605096000000000", - "330917147101019000000000", - "18191421309341000000000", - "9339485898073720000000", - "36445873390924100000", - "0", - "0", - "4587515163153880000000000", - "6248635277046640000000", - "0", - "11595543299043700000000", - "87306471808830700000", - "108140990718061000000000", - "0", - "244839525922229000000000", - "26485198856737900000000", - "93706112341160700000000", - "4093288417193520000000", - "1826933268278630000000", - "31727621966215800000000", - "2598268918741230000000000", - "103916423808888000000000", - "137639145073416000000000", - "116950944093890000000000", - "32586671209230800000000", - "10927155049242900000000", - "20330597187170400000000", - "118498369206000000000000", - "4855495152873640000000", - "0", - "28314442914342700000000", - "6758690237225820000000", - "441359322320000000000000", - "135307625471539000000000", - "123807374310284000000000", - "118498369206000000000000", - "31132043868496300000000", - "30089325503653400000000", - "16764752912776000000000", + "275621478660415000000000", + "18512937102426400000000", + "1699152113577330000000", + "14107755825235700000000", + "142052051646188000000000", + "3849062032671880000000", + "8089315025900530000000", + "70709209729438100000000", + "52291596102134300000000", "150534636946320000000000", - "213878988826899000000000", - "21156650317724300000000", - "14177960459979000000000", - "78578607059630300000000", - "3197537446589480000000", - "623675627400000000000", - "19977562088223700000000", - "566193950263240000000000", - "414218733301934000000000", - "16769461505475200000000", - "5516991529000000000000", - "2560936861229880000000", - "4145732130925890000000", - "94685905467844900000000", - "989000931209480000000", - "4359145017714670000000", - "20705179634139600000000", - "189040818139491000", - "30102468373021800000000", - "97443696296615500000000", - "7437206240716510000000", - "619828448148000000000000", - "458717861762611000000", - "5460046094798400000000", - "15876456448252100000000", - "0", - "62367562740000000000", - "4882773486907200000000", - "59782402187610800000000", - "56741584537930000000000", + "2049134102901940000000", + "9159239661691620000000", + "93342772393302600000000", + "95235059617179600000000", + "14266162256970300000000", + "20091964060643300000000", + "3334827738304940000000", + "54820002947182600000000", + "2974847393597380000000", + "2181156174370190000000", + "8306502164778580000000", + "67614797897081600000000", "1244851039225290000000000", - "0", - "8163489555714540000000", - "73296571200977100000000", - "0", - "15348751087968800000000", - "2089704255183530000000", - "79876380705063700000000", - "3184863620497910000000", - "0", - "0", - "0", - "0", - "5170797490397870000000", - "726123903550606000000", - "7668836838172590000000", - "23886036047548300000", - "107370762436332000000000", - "134046375699461000000000", - "244288974700026000000000", - "1915765998377550000000", - "60701162352704100000000", - "530235470031585000000000", - "2760036019587230000000000", - "1867432787334870000000", - "3815705450175260000000", - "91513959963447400000000", - "13086983515581600000000", - "733472616336446000000000", - "24146417262540800000000", - "3010413407466750000000000", - "0", - "361352522085377000000000", - "8404486757172640000000", - "21516757418197000000000", - "427836876660979000000000", - "44448730144654500000000", - "462818392845554000000000", - "25322991118110000000000", - "2890290787317720000000", + "575175324616419000000", + "63247897000361400000000", + "82342167871210300000000", + "24890966651349300000000", + "180868429148327000000000", + "174230025208806000000000", + "8665122206282830000000", "47230768421157200000000", - "52854397528472100000000", - "250970406239765000000", - "138051687889640000000000", - "9693162569062110000000", - "20554178509366500000000", - "413064291555381000000000", - "168030694870841000000000", - "18512937102426400000000", - "1677596710740750000000", - "3406945189686620000000", - "5717727865576500000000", - "0", - "40535739239168500000000", - "15310781442196600000000", - "161122799773700000000000", - "2270473595840660000000", - "2055336969614260000000000", - "9527138103705070000000", - "2874081232258060000", - "14641417807794300000000", - "13815729863386800000000", - "2897942078968040000000", - "32678929338330500000000", - "4719707450594590000000", - "4595178863351950000000", - "181925888793922000000000", - "44602631707175200000000", - "0", - "2319619466396040000000", - "1501429162236390000000", - "5283851414926280000000000", - "7080713591296290000000", - "2240293400748590000000000", - "155778727441359000000000", - "49193609366080100000000", - "656644721180039000000000", - "152552075991877000000000", - "103665915936931000000000", - "0", - "1177709085530680000000000", + "4640232073794480000000", + "243542559211004000000000", + "1866933021434270000000", + "47576346391581600000000", + "10032630514761100000000", + "551699152900000000000", "647437628070758000000000", - "67402262091869200000000", - "82342167871210300000000", - "2267365382999500000000", - "497225154003423000000000", - "1329091785462440000000", - "146001449155250000000000", - "4509951345441450000000", - "362840753042739000000", + "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", - "67221528842072000000000", + "1197565806739770000000", + "1094862410343480000000", + "118656768591027000000000", + "692690184173425000000000", + "14114973868887700000000", + "289587644026561000000000", + "126576042248673000000000", + "513999515673778000000000", + "9215333791211420000000", + "177733844538762000000", + "11591530259532900000000", + "147803538326405000000", + "51830254294903700000000", + "709633092784286000000000", + "31216560743583900000000", "464045686332740000000", - "0", + "50008923039811900000000", + "8223437992145110000000", + "77467056736236900000000", + "9678177198786450000000", + "147588013048442000000000", + "173585420351260000000000", + "27741200261187100000000", + "4485461892041390000000", + "82554674162924200000000", + "2240293400748590000000000", + "52711726085449700000", + "205010387686959000000000", + "131906804521306000000000", + "87330021557646900000", + "95649434471199700000000", + "75581207624785500000000", + "3282493148280520000000", + "212084325928238000000000", + "4413593223200000000000", + "15348751087968800000000", + "58476852905876400000000", "8574693956652880000000", - "670486333472340000000000", - "0", - "2334790815072800000000", - "0", - "59029517804540300000000", - "132144599756815000000000", - "153862511349028000000000", - "11693881280643000000000", - "0", - "57903949218111900000000", - "3414699755345980000000", - "6417358094545710000000", - "55575822529979000000000", - "2238110431808110000000", - "8003218607368190000000", - "70709209729438100000000", - "2358367707157810000000000", - "6144481509093220000000", - "41609166948708700000", - "47088819587517500", - "307955409215662000000000", - "605566703787214000000000", - "19026196761469100000000", - "1869503490622440000000000", - "25693172931691600000000", - "7728375931562630000000", - "890001976397838000000", - "16030916983366000000000", - "66277048896055500000000", - "3558356529482180000000", - "0", - "958667216652177000000", - "99417919518572700000000", + "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", - "41806840332245700000000", - "5516991529000000000", - "9159239661691620000000", - "693300680726532000000", - "57277145397850400000000", - "5045812374935730000000", - "0", - "4597801668450600000000", - "8686064400485200000000", - "325795610811556000000000", - "298457711206604000000000", - "0", - "0", - "2166048860059500000000", - "60229200324076400000000", - "5218243034178980000000", - "8631621928160100000000", - "24304101265732100000000", - "0", - "6677175238744580000000", - "3476958916542650000000", - "33548138831219600000000", - "30284073672737400000000", - "0", - "137046078112563000000000", - "180420962352139000000000", - "1207557564293390000000", - "152134916100106000000000", - "82085453198239300000000", - "15086586308956200000000", - "17989302669366400000000", - "74301299340506800000000", - "1379247882250000000000", - "10672989700775100000000", - "143741207033173000000000", - "1932662793532890000000", - "700833119289612000000000", - "6302130727043660000000", - "436318663517496000000", - "7521779433770290000000", - "63324542632398200000000", - "542335112537305000000000", - "1106203630048560000000", - "1314530300251970000000000", - "1248578289847130000000", - "346874741542099000000000", - "321227023455753000000000", - "76743755439844100000000", - "10540118937269400000000000", - "0", - "4636152221990680000000", - "8218627115030320000000", - "5911525294404440000000", - "0", - "58958207760280200000000", - "67495373958634400000000", - "25628929349410000000000", - "0", - "41462108300513800000000", - "1939268824151900000000", - "29898938166674500000000", - "23663914854659800000000", - "10910420738298800000000", - "18673284224398500000", - "223065763551343000000000", - "255147848852009000000", - "7711103696993470000000", - "281513658805107000000000", - "232724281550179000000000", - "7530230794001360000000", - "62820404900654900000000", - "40281910132973800000000", - "551699152900000000000", - "55089072045219900000000", + "13997528659387100000000", + "1271244273955960000000000", + "6202234905032390000000", + "56533058147722100000000", + "111056930117748000000000", + "20241476527335100000000", + "13359579297670800000000", + "244700899092545000000000", + "21308537281240500000000", + "12090613000313000000000", + "1172068573646600000000", + "10703653231714000000000", + "55575822529979000000000", + "1980564362708480000000", "7317160501695160000000", - "10761297176744600000000", - "3450841555014860000000", - "3360233647776120000000", - "2453695529650880000000", - "63757361830922500000000", - "2719727715917840000000", - "40678788853989400000000", - "13963868510620700000000", - "195892582467667000000000", - "0", - "57978897639680500000000", - "27050666131122200000000", - "24890966651349300000000", - "5556913405496800000000", - "133465040612635000000", - "766520138646937000000", - "254584453923253000000", - "1306732922840100000000000", - "9215333791211420000000", - "56384777367857500000000", - "37633754933653600000000", - "10857607824309700000000", - "36358271148613500000000", - "0", - "20617989420098800000000", - "140290778350807000000000", - "29487166275538500000000", - "572995205396250000000000", - "1530490881737310000000", - "19626288016197800000000", - "69763795277889100000000", - "116743639468912000000000", - "1726063321587050000000", - "537589152403100000000", - "3758292123854260000000", - "900465634146997000000000", - "9934186523353710000000", - "4817104553381840000000", - "3357655400176220000000", - "709431026167500000000", - "415167366214257000000", + "20227317726314600000000", + "89469155203863600000", "13720893192631300000000", - "13176166038595100000000", - "43764794410311600000", - "496529237610000000000", - "228265423923561000000000", + "19026196761469100000000", + "1932662793532890000000", + "1114626828757660000000", + "93706112341160700000000", "7258008897629410000000", - "160672704290683000000000", - "62215154972109700000000", - "5479356497167540000000", - "276469909500170000000", - "3160135289112300000000", - "56958399714215400000000", - "11847448988134600000000", - "316530397397200", - "7930134903943520000000000", - "439522164140843000000000", - "62642509572441500000000", - "4642552777951680000000", - "494123055049631000000000", - "18002325707379100000000", - "0", - "33744584517779600000000", - "0", - "5021765620317800000000", - "6771410756017490000000", - "2683045248180470000000", - "551975002476450000000000", - "189106287284016000000000", - "2483089998460740000000", - "118928921343591000000000", - "354264735982082000000000", - "1247351254800000000000000", - "147588013048442000000000", + "15447576281200000000000", + "13484878430270300000", + "3407075238484640000000000", + "31334630701722900000000", + "715717819978378000000", + "2134219401197360000000", + "786573524228323000000", + "6227034515573540000000", + "67424392151351400000000", + "6658869261752680000000", + "413064291555381000000000", + "5759739156276000000000", + "4301182051405710000000", + "601675733095054000000000", + "2238110431808110000000", + "237317491139830000000000", + "191278861162104000000000", + "62367562740000000000", + "473372782313472000000000", + "2430384261502710000000", + "23863111755700700000000", + "152552075991877000000000", "40076584307330900000000", - "1481412296011410000000", - "61250360574331700000", - "4569872748776970000000", - "4404188788835030000000000", - "182877076123405000000000", - "2712713337957580000000", - "0", + "583664555460000000000000", + "333324952483953000000", + "33249493167930000000000", + "449508744821480000000000", "7496096215900450000000", - "108196425586330000000000", - "67614797897081600000000", - "35123622593951300000000", - "17590720808092200000000", - "376283677271250000000000", - "19868241143392600000000", - "0", - "1699152113577330000000", - "52002041917772900000000", - "26048456354742000000000", - "23851536551670100000000", - "77685948751583400000000", - "99585879846933700000000", - "3456047885170630000000", - "260934453702493000000000", - "0", - "110284363758587000000000", - "9355134411000000000000", - "1012284725145800000000", - "3064438709745860000000", - "5441039668007030000000", - "7851935249163770000000", - "109161925533307000000000", - "0", - "11267143896183100000000", + "3962056266878050000000", + "7711103696993470000000", + "459300928906112000000000", + "57367761437442100000000", + "10761297176744600000000", + "246815719201231000000000", + "52549734378127800000000", + "224668754461738000000000", + "9527138103705070000000", + "7693055835617630000000", + "2725708179039980000000", + "21262110630747100000000", "4679139621075990000000", - "17631549019466700000000", - "1248606083393260000000", - "785163046948008000000", - "497078663329668000000", - "537167877373048000000", - "50408487116633800000000", - "72712313393497800000000", - "472635698997054000", - "30339948396869700000000", - "0", - "41377436467500000000", - "6620389834800000000000", - "3603793106619780000000", - "0", - "75581207624785500000000", - "62370246764068100000000", - "0", - "4301182051405710000000", - "38469952873549600000000", - "560971621803620000000", - "5190913829028880000000000", - "4629686143798690000000000", - "11083970096246600000000", - "39763068994925500000000", - "0", - "20949744620196400000000", - "0", - "0", - "0", - "10032630514761100000000", - "745699775464738000000000", - "0", - "17711011865310600000000", - "1662132116583680000000", - "21546982669019200000000", - "10075310500300000000000", - "10062991468376900000000", - "236745570164928000000000", - "233996243923637000000000", - "365281825563161000000000", - "1114626828757660000000", - "33853980832512000000000", - "0", - "311837813700000000000000", - "9750476154452590000000", - "11591530259532900000000", - "683309704436992000000", - "7251475167310520000000", - "343619955210112000000", + "3873004793541700000000", "68437218614349800000000", - "0", - "0", - "47258709202126200000000", - "635095463962877000000", - "5504368172469050000000", - "161118348426052000000000", - "5335775079605430000000", - "0", - "47706376969198000000000", - "6125432000199170000000", - "149107879162162000000000", - "6470842294967830000000", - "4193532551074860000000", - "2725708179039980000000", - "2909402099164420000000000", - "68933106279590100000000", - "2439018924335660000000000", - "11229847434150800000000", - "0", - "2083498339228890000000", - "8358774383481750000000", - "45019594193004400000000", - "3008290099693690000000000", - "124290376395421000000000", - "6346816423435040000000", - "139348527974419000000000", - "177733844538762000000", - "2583100386752820000000", - "18650933713748700000000", - "52291596102134300000000", - "35542793994174300000000", - "288837525341637000000000", - "12093247272125100000000", - "279999999697448000000000", - "69248118232244800000000", - "2057611795499480000000", - "7566725060042630000000", - "5826851346679480000000", - "189958632420859000000000", - "1257441999169940000000", - "8306502164778580000000", - "3861894070300000000000", - "8731458783600000000000", - "3766166944302650000000", - "247135546239874000000000", - "6750117813773950000000", - "118656768591027000000000", - "46379507913656500000000", - "161245666330240000000000", - "7066395121143970000000", - "326939410885287000000", - "1393254454549020000000000", - "62735447275653600000000", - "0", - "402477245215322000000000", - "14146832146360200000000", - "12525748785796700000000", - "6500758775779740000000", - "158125486018951000000000", - "11872919270263600000000", - "1661134296992660000000000", - "7024701154571790000000", - "289587644026561000000000", - "775603352121724000000000", - "221417613944964000000000", - "64615053289051300000000", - "35878952366227300000000", - "583664555460000000000000", - "1987386311548830000000", - "0", - "2544671673136560000000", - "3081045980486930000000", - "513999515673778000000000", - "0", - "27251318918576300000000000", - "27157498752969200000000", - "2945589355194260000000", - "2513308256126880000000000", - "0", - "108110716599716000000000", - "13796529756035000000000", - "59173270305729600000000", - "1007150569954920000000", - "74094636632837400000000", - "942057930048386000000", - "92099048516369000000000", - "41309904396315200000000", - "15878429862297600000000", - "6213559786128670000000", - "503416719374400000000000", - "52687053013748200000", - "1193166171524210000000", - "204466313757282000000", - "939156864774516000000", - "653785161726325000000", - "12943219297299200000000", - "26963447923271100000000", - "56773536766884300000000", - "40529569737955000000000", - "27741200261187100000000", - "90966776763469400000", - "32456178538701500000000", - "17616412904992400000000", - "1991065750547680000000", - "5370277944628170000000", - "623200758989813000000000", - "351476091044834000000", - "36960000000000000000000000", - "2685487848666520000000", + "8043874342621060000000", + "4636152221990680000000", "683717965791367000000", - "203820306954279000000", - "18018895435485700000000", - "443800257480574000000", - "430567642292964000000", - "623675627400000000000", - "2201122552892460000000000", - "711933003774673000000000", - "59954640388711000000000", - "4292161788683470000000", - "92894842122204400000000", - "32167028030181100000000", - "27217828055363400000000", - "20276660219257000000000", - "33765705341224100000000", - "14211573538965400000000", - "7196420051622580000000", - "5035103173238540000", - "0", - "5352597742770740000000", - "37349496000634100000000", - "10114759733694000000000", - "0", - "125390793645290000000000", - "11442932212307900000000", - "27574842047325200000000", - "104977424661145000000000", - "439429719099919000000", - "782899706560652000000000", - "22727957368226200000000", - "6235402297341530000000", - "31588176163169900000000", - "1618943456370440000000000", - "282841242906856000000000", - "623675627400000000000000", - "24988144893759100000000", - "63911969840666000000000", - "3750606761140710000000", - "269594740353681000000", - "594523498180657000000000", - "20800499749141400000000", - "164496488537501000000", - "21204971331600000000000", - "129986165862750000000000", - "0", - "3636894368310540000000000", - "1243797802439850000000", - "1236555222253430000000", + "334407398491810000000", "37268456888839100000000", - "5000748842218520000000000", - "5516991529000000000000", - "24510070218194500000000", - "10569012153755200000000", - "26997393461831500000000", - "317378523451694000000000", - "7693055835617630000000", - "4241088232959340000000", - "13997528659387100000000", - "33336687944000000000000000", - "8063178679564190000000", - "40671542441210800000000", - "17835419656239700000000", - "18538093043059700000000", - "18442529688075700000000", - "868422904191896000000", - "0", - "98248528522581600000000", - "1247285812332510000000000", - "83358408212972900000000", - "331451379576926000000000", - "5590676321047540000000", - "10766285141530500000000", - "18361508301700700000000", + "9932915823009380000000", + "24457226639740700000000", + "1618943456370440000000000", + "22648090163373500000000", + "4269934428752100000000", + "17711011865310600000000", + "1527050294915700000000000", + "40828309333125800000000", + "56958399714215400000000", + "3766166944302650000000", + "292471746502122000000000", + "6235402297341530000000", + "5584649470856220000000", + "2028860796954020000000", + "180420962352139000000000", + "2909402099164420000000000", + "18168698391743700000000", + "40529569737955000000000", + "3060352798670210000000", + "37743394689708900000000", + "94519726245531000000000", + "13134889899603500000000", "7657005540706020000000", - "10614825227652900000000", - "19845142836209100000000", - "1808659319460000000000", - "2093973607745550000000000", + "40458906900611500000000", + "3316637015206150000000", + "1400162681606850000000", + "613489458024800000000", + "20748302140643900000000", + "2890290787317720000000", + "8861801571017660000000", + "565699216017207000000", + "24146417262540800000000", + "1243797802439850000000", + "33890998433836900000000", + "241239562971118000000000", + "142788118763215000000000", + "8253212337835790000000", + "45019594193004400000000", + "55087034315589200000000", + "32678929338330500000000", + "73296571200977100000000", + "337121961632892000000000", + "9508445473348790000000", + "11449700986064300000000", "1494315028945280000000", - "51093938054441600000", - "1933806067537780000000", - "20627730385271500000000", - "36686566471121700000000", - "10403491363462100000", - "1300015995345370000000000", - "24513933834222500000000", - "170607045936850000000000", - "50008923039811900000000", - "36084860024793900000000", - "6332461880828440000000", - "23835728528604600000000", - "0", - "868008146837614000000000", - "2430384261502710000000", - "8829475290051100000", - "0", - "198758124444544000000000", - "74587842817507500000000", - "42308835564903500000000", - "27398126416291600000000", - "54065914977680700000000", - "520138550252212000000000", - "36595439061748500000000", - "649540209399021000", - "57746166854367800000000", - "171614985779596000000000", - "44351508366021500000000", + "34294254265343800000000", + "67402262091869200000000", + "18673284224398500000", + "139999348243681000000", + "13082477238239500000000", + "6677175238744580000000", + "647925711065999000000", + "28173702954232800000000", "805288269736064000000000", - "67424392151351400000000", - "692690184173425000000000", - "805031767673847000000000", - "0", - "0", - "4988294448287820000000", - "49132658386042300000000", - "11166855323726400000000", - "3849062032671880000000", - "53516443489640600000000", - "25820186581658100000000", - "6399710173640000000000", - "688785864746820000000", - "872893930745320000000", - "15397326392706900000000", - "192399371921233000000000", - "231713644218000000000000", - "110317801534869000000000", - "549183566011821000000000", - "62435302866994700000000", - "3009338650153920000000", - "441009645611431000000000", - "87440305027490200000000", - "799399799598261000000", - "6409904882159680000000", - "56909285632588000000000", - "27584957645000000000000", - "601675733095054000000000", - "2386460047785010000000000", - "992120897165407000000000", - "6163161100109590000000", - "767268775591811000000000", - "420567988043811000000", - "57367761437442100000000", - "2132510610658660000000", - "10654646923203900000000", - "29795536780931000000000", - "5594572764722470000000", - "0", - "46893270767860800000000", - "6202234905032390000000", - "871138201533291000000000", - "0", - "17428418605156900000000", - "477585243602979000000000", - "20227317726314600000000", - "14471163287713900000000", - "10703653231714000000000", - "10482158025860600000000", - "0", - "108099408931003000000000", - "19385295508025800000000", - "6431450317725490000000", - "33249493167930000000000", - "87546093709663500000000", - "60632360308551800000", - "1172068573646600000000", - "102925909648829000000", - "8818517383907610000000", - "29681420831261600000000", - "217645315819050000000000", - "678451993050976000000", - "861530379886929000", + "27574842047325200000000", "84699739411609700000000", - "0", - "76044716290485800000000", - "102572838685266000000000", - "6227034515573540000000", - "854442954503779000000000", - "8946366845719560000000", - "11394661581846700000000", - "4869602518408980000000", - "27095108594235200000000", - "52804917873774900000000", - "4178327815431760000000", - "29821575832432400000", - "815762452681420000000", - "24116443557079600000000000", - "647925711065999000000", - "1489289568147310000000", - "7353578009524390000000", - "15425704969623800000000", - "2237473003613420000000", - "46860690151916100000000", - "10596543793083700000000", - "7986679305196730000000", - "1139740245004380000000", - "0", - "33880225382928400000000", - "131906804521306000000000", - "0", - "25500395642280400000000", - "204891956655247000000000", - "0", - "133121249255903000000", - "111009462239370000000000", - "74092336960358200000000", - "0", - "41171818556957100000", + "871138201533291000000000", + "855235980495858000000000", + "6758690237225820000000", + "2132510610658660000000", + "6409904882159680000000", + "47706376969198000000000", + "17088746616896800000000", + "9283745059210230000000000", + "24577598495513400000000", + "40202561246068400000000", + "10867989716112600000000", + "53959942136954100000000", + "55169915290000000000000", + "82085453198239300000000", + "29752318498935300000000", + "5516991529000000000000", + "14177960459979000000000", + "92738211802076600000000", + "77088072125010300000000", + "4595178863351950000000", "3389793521582190000000", - "43472162393445700000000", - "174230025208806000000000", - "62962410996398600000000", - "199869328312878000000", - "649709048630229000000", - "557856241008564000000", - "2280353692899730000000", - "0", - "14153570020634400000000000", - "4009580937701170000000", - "4263263809122940000000", - "71680113299376700000000", - "0", - "20241476527335100000000", - "35198330680493300000000", - "40911612366273700000000", - "4711792235347860000000", - "24007187554913100000000", - "45545736375416000000000", + "4760292915833340000000", + "15980201917275600000000", + "4609365819012110000000", + "5700073658677300000000", + "7986679305196730000000", + "412573818588267000000", + "34633772175548800000000", + "3907878384912320000000000", + "31132043868496300000000", + "11423714762203500000000", + "90404501125009400000000", + "15015222843787300000000", + "16764752912776000000000", + "9732979549496000000000", + "87393343794551000000000", + "328314505789069000000", + "62367562740000000000000", + "1718985616188750000000000", + "5460046094798400000000", + "3603793106619780000000", + "2159077793577170000000", "26097249750470200000000", - "579968268047887000000000", - "568428604967443000000000", - "8786666325069280000000", - "34659805511953500000000", - "2405601971892220000000", - "2824247635485830000000", - "0", - "55169915290000000000000", - "4881377695642260000000", - "674866421614604000000000", - "22648090163373500000000", - "5647349772105750000000", - "4474282702186100000000", - "11361492569798100000000", - "91804687990722400000000", - "10821810445189800000000", - "127586032913057000000000", - "4748462230381290000000000", - "5081544105840680000000000", - "3483725003593700000000", - "578033831580453000000000", - "1516613549471860000000", - "12090613000313000000000", - "2955587703951390000000", - "66431647602237500000000", - "1459899806234540000000", - "15613421160060900000000", - "551699152900000000000", - "195345411731964000000000", - "694862327880939000000", - "2677631265999160000000000", - "16807914065802500000000", - "27825128297290500000000", - "9816100994581990000000", - "823843425658521000000000", - "0", - "449508744821480000000000", - "8253212337835790000000", - "26190296843245600000000", - "142052051646188000000000", - "20382528641986400000000", - "1737279587294630000000", - "366646159906488000000000", - "133769187659231000000000", - "337121960756757000000", - "8089315025900530000000", - "862654894866303000000000", - "6827292171047310000000", - "502954593146370000000", - "71720889877000000000000", - "144660492537874000000000", - "21262110630747100000000", - "1226438967148610000000000", - "29821575832432400000", - "10507124271784400000000", - "438094612863659000000000", - "1866933021434270000000", - "6669120764162440000000", "12015265365723600000000", - "0", - "10267365269000000000000000", + "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", - "5700073658677300000000", - "62557806498236500000000", - "58476852905876400000000", - "56301749276264500000000", - "54746875478979400000000", - "34428508851978600000000", - "36459529393105000000000", - "748410752880000000000", - "226665910497828000000000", - "2358725047629300000000", - "190201980837499000000000", - "66622443095591000000000", - "33900249802715400000000", - "93342772393302600000000", - "4509341965208960000000", - "47576346391581600000000", - "11142569962465400000000", - "214560836650147000000", - "827548729350000000000", - "7079316475236830000000", - "40828309333125800000000", - "12550548193135000000000", - "223140393076926000000000", - "23324153840095800000000", - "0", - "857378547531889000000", - "696316866836138000000000", - "205010387686959000000000", - "69767014448870500000000", - "8680485128464710000000", - "31334630701722900000000", - "22474568391687300000000", - "621276952450972000000", - "43927844405166100000000", - "35360793253669400000000", - "237317491139830000000000", - "623675627400000000000", - "425732796288115000000000", - "206810838045840000000000", - "8043874342621060000000", - "238042931908904000000000", - "110782230700061000000000", + "4404188788835030000000000", + "71720889877000000000000", + "4509951345441450000000", "5936520375832220000000", - "23009934010461400000000", - "355282560821282000000", - "81549692447463500000000", - "241239562971118000000000", - "0", - "1221984416224330000000000", - "90404501125009400000000", - "5759739156276000000000", - "9283745059210230000000000", - "436535287368122000000", - "3334827738304940000000", - "19832069695685500000000", - "7274184092902040000000000", + "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", - "11831827950368600000000", + "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", - "52549734378127800000000", - "92385429654756200000000", - "41238442058216600000000", - "5191547821152380000000", - "50817677020074500000000", - "181698909615612000000000", - "0", - "40634798904379300000000", - "10852772252454200000", - "41397906368637400000000", - "0", - "477543412477717000000000", - "0", - "534366721513041000000", - "0", - "0", + "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", - "6276963414379310000000", - "4965292376100000000000", - "21320545270648700000000", - "24835598767229900000000", + "4193532551074860000000", + "107370762436332000000000", + "1257441999169940000000", + "2531880102781820000000", + "244288974700026000000000", + "168030694870841000000000", + "3008290099693690000000000", + "45652039021281900000000", + "103665915936931000000000", + "441359322320000000000000", + "3164396764081920000000", + "12593159980195300000000", + "23706576961575300000000", + "4093288417193520000000", + "182877076123405000000000", + "25970759532457600000000", + "2709507925774800000000", + "6236928262017940000000", + "3566619535925950000000000", + "44602631707175200000000", + "2478684148059030000000000", + "5805699003800880000000", + "6620389834800000000000", "1065369126151080000000", - "34911148484410300000000", - "278744597268628000000000", - "13126384251428100000000", - "185234251688991000000000", - "25089580990323000000000", - "262251850968558000000000", - "14114973868887700000000", - "9944912395284720000", - "2181156174370190000000", - "47201000208634300000000", - "0", - "8239517774300360000000", - "1485223775584830000000", - "1158568221090000000000", - "107503387238580000000000", + "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", - "2028860796954020000000", - "5922153908104060000000000", - "275849576450000000000000", - "88589021640737300000000", - "82162015847396900000000", - "10867989716112600000000", - "11033983058000000000000", - "1980564362708480000000", - "26714715143082900000000", - "57283170957287000000", - "1073495212719060000000", - "31556394306966100000000", - "0", - "41178121085419500000000", - "0", - "79622565502832000000000", - "0", - "28389561591464300000000", - "2775789416304410000000", - "57742073584300900000000", - "13082477238239500000000", - "0", - "21081086244974800000000", - "695730310208492000000", - "586969776996768000000000", - "273669690263550000000", - "3371219607567570000000", - "95452925481154800000", - "2145135710611280000000", - "67186878315993800000000", - "3838657796592000000000", - "54820002947182600000000", - "0", - "22469042689002100000000", - "333324952483953000000", - "2689900009085770000000", - "5332067332530980000000", - "46806646245060900000000", - "321188331798819000000", - "27989850627387800000000", - "842804901891892000000000", - "15175051724273800000000", - "59725526390478700000000", - "0", - "19001970790529000000000", - "5964319070114180000000", - "4706404841219600000000", - "13384147159402400000000", - "89469155203863600000", - "224686935012569000000000", - "9573823775045300000000", - "7921118064188510000000", - "1127039169205910000000", - "17389658160308800000000", - "525829755165305000000000", - "63620463595498500000000", - "12903946187208100000000", + "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", - "0", - "0", - "23143988359173400000000", - "14564623548612500000000", - "0", - "3713826227325340000000", - "427083920060770000000", - "2909402099164420000000000", - "5965393163047930000000", - "1666828635550010000000", - "0", - "98682780240710400000000", - "715717819978378000000", - "13795418145507600000000", - "5765439823852850000000", - "112192991968260000000000", - "445113781551096000000000", - "6283896919871620000000", - "1031686164147290000000", - "1446575346537260000000", - "4549022714839540000000", - "92738211802076600000000", - "45135889572286000000000", + "1703001034876280000000", "234113887712637000000000", - "4812527162714520000000", + "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", - "0", - "445499573633412000000000", - "1197565806739770000000", - "838320088168542000000000", - "474918798654353000000000", - "2758309070230680000000", - "53959942136954100000000", - "4655362664294900000000", - "459300928906112000000000", - "11270118853964700000000", - "58522100492148700000000", - "9273068414413280000000", - "3779657547173460000000", - "1213639058724320000000", - "101748198089111000000000", - "43906552380097700000000", - "9732979549496000000000", - "74163268067312100000", + "236745570164928000000000", + "16668101089553200000000", + "108099408931003000000000", + "11831827950368600000000", + "376283677271250000000000", + "35360793253669400000000", + "41397906368637400000000", + "92385429654756200000000", "3652303362764770000000", - "147803538326405000000", - "113100133618500000000000", - "2134219401197360000000", - "3628060891875000000000000", - "106162039948469000000000", - "574420029339790000000", - "29821575832432400000", - "118974145929408000000000", - "30183487811052200000000", - "0", - "0", - "2758495764500000000000", - "33254469976715200000000", - "0", - "35118650089838200000000", - "157096755808780000000000", - "244700899092545000000000", - "6080239250030900000000", - "173585420351260000000000", - "2076494452153400000000", - "8305806296609820000000", - "3382464548522960000000", - "498162771815240000000", - "58582844363693900000000", - "212084325928238000000000", - "13248222144824900000000", - "3060352798670210000000", - "4483464851212950000000", - "3371219607567570000", - "30572293671637100000000", - "0", - "38607351902104600000000", - "117972266877242000000000", - "0", - "50657868716836000000000", - "4760292915833340000000", - "6177583703817700000000", - "22116511541065300000000", - "19179823892471600000000", - "11093280720573500000000", - "50568294113513500000000", - "11991110936198700000000", - "0", - "19309470351500000000000", - "62367562740000000000000", - "1703001034876280000000", - "2256250830244480000000", - "50347989115403700000000", - "162392775001216000000000", "225604865585414000000000", - "0", - "3756588175861420000000", - "168872311661191000000000", - "9508445473348790000000", - "13393278133411900000000", - "49725625571970900000000", - "26464199440915000000000", - "885349637532426000000", - "0", - "532849207640182000000", - "13994971478188900000000", - "3316637015206150000000", - "104366566517494000000000", - "0", + "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", - "2206796994903020000000", - "86570779540312200000000", - "0", - "11449700986064300000000", - "399258383099847000000", - "502645201090091000000000", + "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", - "2683000703227400000000", - "2430109130535250000000", - "73329144584281600000000", - "2568645832740220000000", - "16449957088669200000000", - "47567909826813400000000", + "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", - "21859829044974900000000", - "0", - "180868429148327000000000", - "71948876876004900000000", - "20497093575018400000000", - "6236928262017940000000", - "855235980495858000000000", - "224668754461738000000000", - "8223437992145110000000", - "45652039021281900000000", - "551699188183930000000", - "95235059617179600000000", + "2824247635485830000000", + "784563374179046000000000", + "2055336969614260000000000", + "127586032913057000000000", + "649540209399021000", "34113165830185200000000", - "23706576961575300000000", - "524875370516281000000", - "8979599636660670000000", - "20091964060643300000000", - "18168698391743700000000", - "7324318513800240000000000", - "3164396764081920000000", - "55169915290000000000000", - "122268835855220000000000", - "73763989615444500000000", - "11171907846225000000000", - "134570364871332000000000", + "116743639468912000000000", + "3758292123854260000000", + "1010757784722070000000000", + "496529237610000000000", + "2256250830244480000000", "8631647255920980000000", - "604945583657267000000", - "0", - "217411492269705000000000", - "395066049686813000000000", - "18774359895389100000000", - "7079561175891890000000", - "150431366773843000000", - "56533058147722100000000", - "9678177198786450000000", - "0", - "3873004793541700000000", - "14107755825235700000000", - "567544820934000000000000", - "243542559211004000000000", - "109177143562739000000000", - "172407270353484000000000", - "12096962142869300000000", - "63190450337686900000000", - "371686097382089000000000", - "63252867348451600000000", - "13224284539082200000000", - "0", - "0", - "3945144641889750000000", - "0", - "599410871068563000000000", + "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", - "156713402586954000000000", - "95649434471199700000000", - "40458906900611500000000", - "64672302116482000000000", - "11530919105737300000000", - "10884459496643900000000", - "3558625045950800", - "0", - "5584649470856220000000", - "9023379659335410000000", - "2178181479510010000000", - "214227634222945000000000", - "18867549836601800000000", - "3742053764400000000000", - "8665122206282830000000", - "2307599821380000000000", - "3962056266878050000000", - "1641881960897060000000", - "21284366066595900000000", - "336579867644273000000000", - "0", - "3365246876970250000000", - "18403007455391700000000", - "11618746778510600000000", - "17088746616896800000000", - "1697610675067840000000", - "11595728395592800000000", + "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", - "114643170397382000000000", - "0", - "10357888324877100000000", - "24577598495513400000000", - "1396077042432270000000", - "6658869261752680000000", - "242562290147757000000000", - "18467065069811200000000", - "827548729350000000000", - "4415865405453250000000", - "246686460716218000000000", - "77467056736236900000000", - "1042031103210730000000", - "2909402099164420000000000", - "62498919561229700000000", - "6875905130245960000000", - "119410088509766000000000", - "0", - "88191095511788700000000", - "0", - "13134889899603500000000", - "2909402099164420000000000", - "16686996811056400000000", + "766342772799194000000", + "7521779433770290000000", + "5717727865576500000000", + "4569872748776970000000", + "3174350247238620000000", + "20554178509366500000000", + "27050666131122200000000", + "2544671673136560000000", + "9980331715136730000000", + "1933806067537780000000", + "1031686164147290000000", + "8399869738398540000000", + "5014880734125350000000", + "4292161788683470000000", + "2178181479510010000000", + "4474282702186100000000", + "8818517383907610000000", + "336204948097149000000000", + "10596543793083700000000", + "57283170957287000000", + "497078663329668000000", + "104977424661145000000000", "4155455861340040000000", - "17639989892724900000000", - "97013802876916700000000", - "50705339496860800000000", - "613489458024800000000", - "4609365819012110000000", - "1552460646779970000000", - "18840617028126600000000", - "0", - "48083095825158200000000", - "2884923644033240000000", - "153028789969137000000000", - "191278861162104000000000", - "2809113112498940000000", - "126576042248673000000000", - "12137381363800000000000", - "874404022888118000000000", - "23089282320921700000000", - "551699152900000000000", - "45136333108904900000000", - "33873569923653500000000", - "385291613852874000000", - "161386503498471000000000", - "94519726245531000000000", - "13359579297670800000000", - "1010757784722070000000000", - "412573818588267000000", - "102181941847258000000000", - "10828231442738700000000", - "575175324616419000000", - "5526395946610910000000", + "2057611795499480000000", + "23009934010461400000000", + "11608931927484600000000", + "3476958916542650000000", "37990338066825600000000", - "2904936812607850000000", - "27781032225739300000000", - "0", - "51469615634434400000000", - "15980201917275600000000", - "33890998433836900000000", - "43753003738671800000000", - "76743755439844100000000", - "7038058586046910000000", - "9899448075676560000000", - "1344459099788760000000000", - "7401598730507370000000", - "1906149412229860000000", + "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", - "14025211385968300000000", - "10851908657543200000000", - "7796466187674060000000", + "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", - "11644764491837400000000", - "29752318498935300000000" + "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 From beac12a484e06893f24622f1f293f83055a7effc Mon Sep 17 00:00:00 2001 From: PacificOceanSailor Date: Thu, 8 May 2025 16:17:54 +0330 Subject: [PATCH 22/48] Change penalty and duration --- contracts/vesting/SymmVestingPlanInitializer.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol index fe8b946..54d4252 100644 --- a/contracts/vesting/SymmVestingPlanInitializer.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -31,8 +31,8 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable { // CONSTANTS // ============================================================= - uint256 public constant VESTING_DURATION = 60 days; - uint256 public constant PENALTY_PER_DAY_BP = 25e16; // 2.5% expressed as 0.25 * 1e18 + uint256 public constant VESTING_DURATION = 140 days; + uint256 public constant PENALTY_PER_DAY_BP = 1e17; // 1% expressed as 0.1 * 1e18 // ============================================================= // IMMUTABLES From eac5028c278ee74778a98da263acbe9004272ec3 Mon Sep 17 00:00:00 2001 From: Naveed Date: Mon, 14 Jul 2025 10:21:10 +0200 Subject: [PATCH 23/48] Add buildersNFT --- .gitignore | 5 +- contracts/builders-nft/SymmioBuildersNft.sol | 654 ++++++++++++++++++ .../builders-nft/SymmioUnlockManager.sol | 544 +++++++++++++++ .../SymmAllocationClaimer.sol | 0 .../interfaces/IERC20Minter.sol | 0 .../vesting/SymmVestingPlanInitializer.sol | 4 +- contracts/vesting/VestingV2.sol | 521 ++++++++++++++ package-lock.json | 82 ++- package.json | 3 +- scripts/.python-version | 1 + scripts/future_reset_vesting_plans.py | 498 +++++++++++++ scripts/pyproject.toml | 11 + scripts/reset_vesting_plans.py | 250 +++++++ scripts/user_allocation.py | 60 -- scripts/vesting_setup.py | 641 +++++++++++++++++ 15 files changed, 3203 insertions(+), 71 deletions(-) create mode 100644 contracts/builders-nft/SymmioBuildersNft.sol create mode 100644 contracts/builders-nft/SymmioUnlockManager.sol rename contracts/{claimSymm => claim}/SymmAllocationClaimer.sol (100%) rename contracts/{claimSymm => claim}/interfaces/IERC20Minter.sol (100%) create mode 100644 contracts/vesting/VestingV2.sol create mode 100644 scripts/.python-version create mode 100644 scripts/future_reset_vesting_plans.py create mode 100644 scripts/pyproject.toml create mode 100644 scripts/reset_vesting_plans.py delete mode 100644 scripts/user_allocation.py create mode 100644 scripts/vesting_setup.py diff --git a/.gitignore b/.gitignore index 40a9252..088cef4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ package-lock.json rewards abis -error.log \ No newline at end of file +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..32c73da --- /dev/null +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -0,0 +1,654 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @title SymmioBuildersNft + * @notice Advanced ERC721 NFT contract for locking SYMM tokens on the Base chain to enable + * fee reductions across multiple chains. Each NFT represents a locked amount of SYMM + * tokens with customizable branding and comprehensive unlock management capabilities. + * + * @dev Core features include: + * • SYMM token locking with minimum amount requirements and token burning + * • NFT minting with associated brand names and lock data storage + * • NFT merging functionality to consolidate locked amounts + * • Partial unlock processes via external unlock manager integration + * • Cross-chain synchronization for lock data consistency + * • Fee collector integration for automatic fee reduction calculations + * • Granular pause controls for transfers and contract operations + * • Role-based access control for administrative and sync functions + * • Comprehensive view functions for user and system queries + * + * Integration points include external unlock manager for time-locked releases, + * fee collector contracts for cross-chain fee reduction tracking, and sync + * mechanisms for maintaining consistency across multiple blockchain networks. + */ + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.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 Interface for the unlock manager contract handling token unlock processes. + * @dev Deployed separately to manage unlock operations for locked SYMM tokens. + */ +interface ISymmUnlockManager { + /** + * @notice Initiate the unlock process for a specified NFT. + * @param tokenId Token ID of the NFT to unlock. + * @param owner Owner address of the NFT. + * @param amount Amount of tokens to unlock. + */ + function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external; + + /** + * @notice Check if an NFT is currently in the unlocking process. + * @param tokenId Token ID to check. + * @return Whether the NFT is being unlocked. + */ + function isUnlocking(uint256 tokenId) external view returns (bool); +} + +/** + * @notice Interface for the fee collector contract handling fee collection. + */ +interface ISymmFeeCollector { + /** + * @notice Called when the locked amount of an NFT changes. + * @param amount Change in locked amount (positive for increase, negative for decrease). + */ + function onLockedAmountChanged(int256 amount) external; +} + +contract SymmioBuildersNft is ERC721Enumerable, AccessControlEnumerable, Pausable, ReentrancyGuard { + using SafeERC20 for IERC20; + + /* ─────────────────────────────── Roles ─────────────────────────────── */ + + /// @notice Role for updating configuration parameters like minimum lock amount and unlock manager. + bytes32 public constant SETTER_ROLE = keccak256("SETTER_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"); + + /// @notice Role for pausing/unpausing NFT transfers specifically. + bytes32 public constant TRANSFER_PAUSER_ROLE = keccak256("TRANSFER_PAUSER_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 (immutable for gas efficiency). + IERC20Burnable public immutable SYMM; + + /// @notice The unlock manager contract for handling token unlock processes. + ISymmUnlockManager public unlockManager; + + /// @notice The minimum amount of SYMM tokens required to mint an NFT. + uint256 public minLockAmount; + + /// @notice Counter for generating unique token IDs sequentially. + uint256 private _tokenIdCounter; + + /// @notice Flag indicating whether NFT transfers are paused independently of contract pause. + bool public transfersPaused; + + /// @notice Mapping of token ID to its comprehensive lock data. + mapping(uint256 => LockData) public lockData; + + /// @notice Mapping of user address to their owned token IDs for internal tracking. + mapping(address => uint256[]) private _userTokens; + + /// @notice Mapping of token ID to its related fee collector addresses for fee reduction tracking. + mapping(uint256 => address[]) public tokenRelatedFeeCollectors; + + /* ─────────────────────────────── Events ─────────────────────────────── */ + + /** + * @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. + * @param brandName Brand name associated with the NFT. + */ + event TokenLocked(address indexed user, uint256 indexed tokenId, uint256 amount, string brandName); + + /** + * @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 NFT's brand name is updated. + * @param tokenId ID of the NFT. + * @param newBrandName New brand name assigned. + */ + event BrandNameUpdated(uint256 indexed tokenId, string newBrandName); + + /** + * @notice Emitted when an unlock process is initiated for an NFT. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens to unlock. + */ + event UnlockInitiated(uint256 indexed tokenId, address indexed owner, uint256 amount); + + /** + * @notice Emitted when the minimum lock amount is updated. + * @param newMinAmount New minimum lock amount. + */ + event MinLockAmountUpdated(uint256 newMinAmount); + + /** + * @notice Emitted when the unlock manager address is updated. + * @param newUnlockManager New unlock manager address. + */ + event UnlockManagerUpdated(address newUnlockManager); + + /** + * @notice Emitted when the transfer pause state is updated. + * @param paused New pause state (true for paused, false for unpaused). + */ + event TransfersPausedUpdated(bool paused); + + /** + * @notice Emitted when an NFT is minted for cross-chain synchronization. + * @param to Address receiving the NFT. + * @param tokenId ID of the minted NFT. + * @param amount Amount of SYMM tokens locked. + * @param brandName Brand name associated with the NFT. + */ + event SyncMint(address indexed to, uint256 indexed tokenId, uint256 amount, string brandName); + + /** + * @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); // locked amount below minimum required + error NotTokenOwner(); // caller is not the owner of the NFT + error InsufficientLockedAmount(); // requested unlock amount exceeds available locked amount + error InvalidTokenId(); // invalid token ID provided + error ZeroAddress(); // zero address provided for critical parameters + error ZeroAmount(); // zero amount provided for operations requiring non-zero value + error TransfersPaused(); // NFT transfers are paused + error UnlockManagerNotSet(); // unlock manager is not set + error TokenHasActiveUnlock(); // NFT has an active unlock process + error UnauthorizedAccess(address caller, address requiredCaller); // unauthorized caller attempted restricted action + error LengthMismatch(); // input arrays have mismatched lengths + + /* ─────────────────────────────── Structs ─────────────────────────────── */ + + /** + * @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 brandName Custom brand name associated with the NFT. + * @param unlockingAmount Amount of tokens currently being unlocked. + */ + struct LockData { + uint256 amount; + uint256 lockTimestamp; + string brandName; + uint256 unlockingAmount; + } + + /* ─────────────────────────── Initialization ─────────────────────────── */ + + /** + * @notice Initialize the SymmioBuildersNft contract with core parameters. + * @param _symm Address of the SYMM token contract. + * @param _admin Address to receive admin and all role assignments. + * @param _minLockAmount Minimum amount of SYMM tokens required to mint an NFT. + * + * @dev Sets up the ERC721 contract, assigns comprehensive roles, and validates inputs. + */ + constructor(address _symm, address _admin, uint256 _minLockAmount) ERC721("Symmio Builders NFT", "BUILDERS") { + if (_symm == address(0) || _admin == address(0)) revert ZeroAddress(); + if (_minLockAmount == 0) revert ZeroAmount(); + + SYMM = IERC20Burnable(_symm); + minLockAmount = _minLockAmount; + + // Grant all roles to the admin for initial setup + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(SETTER_ROLE, _admin); + _grantRole(PAUSER_ROLE, _admin); + _grantRole(UNPAUSER_ROLE, _admin); + _grantRole(TRANSFER_PAUSER_ROLE, _admin); + _grantRole(SYNC_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 by transferring to zero address + SYMM.burnFrom(msg.sender, amount); + + // Mint new NFT with the next available token ID + tokenId = _tokenIdCounter++; + _safeMint(msg.sender, tokenId); + + // Store comprehensive lock data for the NFT + lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, brandName: brandName, unlockingAmount: 0 }); + + // Notify all related fee collectors of the locked amount increase + for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) + ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); + + emit TokenLocked(msg.sender, 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. + * + * @dev Burns the SYMM tokens and increases the locked amount for the NFT. + */ + function lock(uint256 tokenId, uint256 amount) external nonReentrant whenNotPaused { + if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); + + // Burn the SYMM tokens by transferring to zero address + SYMM.burnFrom(msg.sender, amount); + + // Increase the locked amount for the NFT + lockData[tokenId].amount += amount; + + // Notify all related fee collectors of the locked amount increase + for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) + ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); + + emit TokenLocked(msg.sender, tokenId, amount, lockData[tokenId].brandName); + } + + /* ────────────────────────── 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). + * + * @dev Combines locked amounts, burns source NFT, updates target NFT, and notifies fee collectors. + * Reverts if either NFT has an active unlock process. + */ + function merge(uint256 targetTokenId, uint256 sourceTokenId) external nonReentrant whenNotPaused { + if (ownerOf(targetTokenId) != msg.sender) revert NotTokenOwner(); + if (ownerOf(sourceTokenId) != msg.sender) revert NotTokenOwner(); + + LockData storage targetData = lockData[targetTokenId]; + LockData storage sourceData = lockData[sourceTokenId]; + + if (targetData.unlockingAmount > 0 || sourceData.unlockingAmount > 0) revert TokenHasActiveUnlock(); + + // Merge locked amounts into the target NFT + uint256 newAmount = targetData.amount + sourceData.amount; + targetData.amount = newAmount; + + // Burn the source NFT and clear its lock data + _burn(sourceTokenId); + delete lockData[sourceTokenId]; + + // Notify fee collectors for the target NFT (increase) + for (uint256 i = 0; i < tokenRelatedFeeCollectors[targetTokenId].length; i++) + ISymmFeeCollector(tokenRelatedFeeCollectors[targetTokenId][i]).onLockedAmountChanged(int256(sourceData.amount)); + + // Notify fee collectors for the source NFT (decrease) + for (uint256 i = 0; i < tokenRelatedFeeCollectors[sourceTokenId].length; i++) + ISymmFeeCollector(tokenRelatedFeeCollectors[sourceTokenId][i]).onLockedAmountChanged(-int256(sourceData.amount)); + + emit TokensMerged(targetTokenId, sourceTokenId, newAmount); + } + + /** + * @notice Update the brand name of an NFT. + * @param tokenId ID of the NFT to update. + * @param newBrandName New brand name for the NFT. + * + * @dev Only callable by the NFT owner. + */ + function updateBrandName(uint256 tokenId, string memory newBrandName) external { + if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); + + lockData[tokenId].brandName = newBrandName; + + emit BrandNameUpdated(tokenId, newBrandName); + } + + /* ──────────────────────── 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. + * + * @dev Updates the unlocking amount, calls the unlock manager, and notifies fee collectors. + */ + function initiateUnlock(uint256 tokenId, uint256 amount) external nonReentrant { + if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); + if (address(unlockManager) == address(0)) revert UnlockManagerNotSet(); + + LockData storage data = lockData[tokenId]; + uint256 availableAmount = data.amount - data.unlockingAmount; + + if (amount > availableAmount) revert InsufficientLockedAmount(); + if (amount == 0) revert ZeroAmount(); + + // Update the unlocking amount + data.unlockingAmount += amount; + + // Delegate to the unlock manager + unlockManager.initiateUnlock(tokenId, msg.sender, amount); + + // Notify fee collectors of the effective decrease in locked amount + for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) + ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(-int256(amount)); + + emit UnlockInitiated(tokenId, msg.sender, amount); + } + + /** + * @notice Complete the unlock process for an NFT, transferring tokens to the unlock manager. + * @param tokenId ID of the NFT to unlock. + * @param amount Amount of tokens being unlocked. + * + * @dev Only callable by the unlock manager. Burns the NFT if no tokens remain. + */ + function completeUnlock(uint256 tokenId, uint256 amount) external { + if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); + + LockData storage data = lockData[tokenId]; + data.unlockingAmount -= amount; + data.amount -= amount; + + // Burn the NFT if no locked tokens remain + if (data.amount == 0) { + _burn(tokenId); + delete lockData[tokenId]; + } + } + + /** + * @notice Cancel an unlock process for an NFT, restoring the unlocking amount. + * @param tokenId ID of the NFT to cancel the unlock for. + * @param amount Amount to cancel from the unlocking process. + * + * @dev Only callable by the unlock manager. Restores effective locked amount. + */ + function cancelUnlock(uint256 tokenId, uint256 amount) external { + if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); + + lockData[tokenId].unlockingAmount -= amount; + + // Notify fee collectors of the effective increase in locked amount + for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) + ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); + } + + /* ───────────────────── Cross-Chain Sync Functions ───────────────────── */ + + /** + * @notice Mint an NFT without token transfer for cross-chain synchronization. + * @param to Address to mint the NFT to. + * @param tokenId Specific token ID to mint. + * @param amount Amount of SYMM tokens locked. + * @param brandName Brand name for the NFT. + * + * @dev Only callable by accounts with SYNC_ROLE. Used for cross-chain lock data sync. + */ + function syncMint(address to, uint256 tokenId, uint256 amount, string memory brandName) external onlyRole(SYNC_ROLE) whenNotPaused { + // Update token ID counter to avoid conflicts with future mints + if (tokenId >= _tokenIdCounter) { + _tokenIdCounter = tokenId + 1; + } + + // Mint NFT to the specified address + _safeMint(to, tokenId); + + // Store lock data for the NFT + lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, brandName: brandName, unlockingAmount: 0 }); + + // Notify all related fee collectors of the locked amount + for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) + ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); + + emit SyncMint(to, tokenId, amount, brandName); + } + + /** + * @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. + * + * @dev Only callable by accounts with SYNC_ROLE. Arrays must have matching lengths. + */ + function batchUpdateLockData(uint256[] calldata tokenIds, LockData[] calldata lockDatas) external onlyRole(SYNC_ROLE) { + if (tokenIds.length != lockDatas.length) revert LengthMismatch(); + + // Update lock data for each token ID + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 oldAmount = lockData[tokenIds[i]].amount; + uint256 newAmount = lockDatas[i].amount; + lockData[tokenIds[i]] = lockDatas[i]; + + // Notify all related fee collectors of the locked amount + for (uint256 j = 0; j < tokenRelatedFeeCollectors[tokenIds[i]].length; j++) + ISymmFeeCollector(tokenRelatedFeeCollectors[tokenIds[i]][j]).onLockedAmountChanged(int256(newAmount) - int256(oldAmount)); + } + } + + /* ───────────────────────── Pause Controls ───────────────────────── */ + + /** + * @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(); + } + + /** + * @notice Set the pause state for NFT transfers independently of contract pause. + * @param _paused True to pause transfers, false to unpause. + * + * @dev Only callable by accounts with TRANSFER_PAUSER_ROLE. + */ + function setTransfersPaused(bool _paused) external onlyRole(TRANSFER_PAUSER_ROLE) { + transfersPaused = _paused; + emit TransfersPausedUpdated(_paused); + } + + /* ────────────────────────── Admin Functions ────────────────────────── */ + + /** + * @notice Set the minimum lock amount for minting NFTs. + * @param _minLockAmount New minimum lock amount. + * + * @dev Only callable by accounts with SETTER_ROLE. + */ + function setMinLockAmount(uint256 _minLockAmount) external onlyRole(SETTER_ROLE) { + if (_minLockAmount == 0) revert ZeroAmount(); + minLockAmount = _minLockAmount; + emit MinLockAmountUpdated(_minLockAmount); + } + + /** + * @notice Set the address of the unlock manager contract. + * @param _unlockManager New unlock manager address. + * + * @dev Only callable by accounts with SETTER_ROLE. + */ + function setUnlockManager(address _unlockManager) external onlyRole(SETTER_ROLE) { + if (_unlockManager == address(0)) revert ZeroAddress(); + unlockManager = ISymmUnlockManager(_unlockManager); + emit UnlockManagerUpdated(_unlockManager); + } + + /** + * @notice Add fee collectors to an NFT for fee reduction tracking. + * @param tokenId ID of the NFT to add fee collectors to. + * @param feeCollectors Array of fee collector addresses to add. + * + * @dev Only callable by accounts with SETTER_ROLE. + */ + 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. + * + * @dev Only callable by accounts with SETTER_ROLE. Uses swap-and-pop for gas efficiency. + */ + function removeFeeCollector(uint256 tokenId, address feeCollector) external onlyRole(SETTER_ROLE) { + for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) { + if (tokenRelatedFeeCollectors[tokenId][i] == feeCollector) { + tokenRelatedFeeCollectors[tokenId][i] = tokenRelatedFeeCollectors[tokenId][tokenRelatedFeeCollectors[tokenId].length - 1]; + tokenRelatedFeeCollectors[tokenId].pop(); + break; + } + } + emit FeeCollectorRemoved(tokenId, feeCollector); + } + + /* ────────────────────────── View Functions ────────────────────────── */ + + /** + * @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 lock data for multiple NFTs in a single call. + * @param tokenIds Array of token IDs to query. + * @return Array of LockData structs. + */ + function getLockDataBatch(uint256[] calldata tokenIds) external view returns (LockData[] memory) { + LockData[] memory result = new LockData[](tokenIds.length); + for (uint256 i = 0; i < tokenIds.length; i++) { + result[i] = lockData[tokenIds[i]]; + } + return result; + } + + /** + * @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); + } + } + + /* ───────────────────────── Internal Helpers ───────────────────────── */ + + /** + * @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. + * + * @dev Supports both ERC721Enumerable and AccessControlEnumerable interfaces. + */ + function supportsInterface(bytes4 interfaceId) public view override(ERC721Enumerable, AccessControlEnumerable) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/contracts/builders-nft/SymmioUnlockManager.sol b/contracts/builders-nft/SymmioUnlockManager.sol new file mode 100644 index 0000000..a1bf263 --- /dev/null +++ b/contracts/builders-nft/SymmioUnlockManager.sol @@ -0,0 +1,544 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @title SymmUnlockManager + * @notice Manages the unlock process for SYMM tokens locked in SymmioBuildersNFT contracts with + * cliff periods and vesting integration. Provides a complete workflow for token unlocking + * including request initiation, cliff enforcement, cancellation capabilities, and vesting setup. + * + * @dev Core features include: + * • Unlock request management with unique ID tracking + * • Configurable cliff period enforcement before token release + * • Integration with external vesting contracts for gradual token release + * • Cancellation functionality for unlock requests during cliff period + * • Comprehensive tracking of unlock status and timing + * • Role-based access control for administrative functions + * • Emergency pause functionality for security incidents + * • Token rescue capabilities for administrative recovery + * • Detailed view functions for unlock request analysis + * + * The contract coordinates between SymmioBuildersNFT for lock management and external + * vesting contracts for token distribution, ensuring secure and controlled token unlocking + * with configurable time-based restrictions and user flexibility. + */ + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/Pausable.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +/* ────────────────────────── External Interfaces ────────────────────────── */ + +/** + * @notice Interface for the SymmioBuildersNFT contract to query ownership and lock data. + */ +interface ISymmioBuildersNft { + /** + * @notice Get the owner of a specific NFT. + * @param tokenId ID of the NFT to query. + * @return Address of the NFT owner. + */ + function ownerOf(uint256 tokenId) external view returns (address); + + /** + * @notice Get the lock data for a specific NFT. + * @param tokenId ID of the NFT to query. + * @return amount Amount of tokens locked. + * @return lockTimestamp Timestamp when tokens were locked. + * @return brandName Brand name associated with the NFT. + * @return unlockingAmount Amount currently being unlocked. + */ + function lockData( + uint256 tokenId + ) external view returns (uint256 amount, uint256 lockTimestamp, string memory brandName, uint256 unlockingAmount); + + /** + * @notice Complete the unlock process for an NFT. + * @param tokenId ID of the NFT to unlock. + * @param amount Amount of tokens to unlock. + */ + function completeUnlock(uint256 tokenId, uint256 amount) external; + + /** + * @notice Cancel an unlock process for an NFT. + * @param tokenId ID of the NFT to cancel unlock for. + * @param amount Amount to cancel from the unlock process. + */ + function cancelUnlock(uint256 tokenId, uint256 amount) external; +} + +/** + * @notice Interface for the Vesting contract to set up vesting plans. + */ +interface IVesting { + /** + * @notice 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 user. + */ + function setupVestingPlans(address token, uint256 startTime, uint256 endTime, address[] memory users, uint256[] memory amounts) external; +} + +contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard { + using SafeERC20 for IERC20; + + /* ─────────────────────────────── Roles ─────────────────────────────── */ + + /// @notice Role for updating configuration parameters like cliff and vesting durations. + bytes32 public constant SETTER_ROLE = keccak256("SETTER_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 The SymmioBuildersNFT contract (immutable for gas optimization). + ISymmioBuildersNft public immutable symmBuildersNft; + + /// @notice The Vesting contract for managing token vesting plans. + IVesting public vestingContract; + + /// @notice The SYMM token contract (immutable for gas optimization). + IERC20 public immutable SYMM; + + /// @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 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. + */ + struct UnlockRequest { + uint256 amount; + uint256 unlockInitiatedTime; + address owner; + uint256 tokenId; + bool cliffPassed; + bool vestingStarted; + } + + /* ─────────────────────────────── Events ─────────────────────────────── */ + + /** + * @notice Emitted when an unlock request is initiated. + * @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 request 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 the cliff period for an unlock request is completed. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + */ + event CliffCompleted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner); + + /** + * @notice Emitted when vesting starts for an unlock request. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens entering vesting. + */ + event VestingStarted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount); + + /** + * @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 the vesting contract address is updated. + * @param newVestingContract New vesting contract address. + */ + event VestingContractUpdated(address newVestingContract); + + /* ─────────────────────────────── Errors ─────────────────────────────── */ + + error NotNFTOwner(); // caller is not the owner of the NFT + error UnlockNotFound(); // unlock request ID is invalid or not found + error CliffNotPassed(); // cliff period has not yet passed + error VestingAlreadyStarted(); // vesting has already started for this unlock request + error InvalidDuration(); // invalid duration (zero) provided for cliff or vesting + error ZeroAddress(); // zero address provided for critical parameters + error ZeroAmount(); // zero amount provided for operations requiring non-zero value + error UnauthorizedAccess(address caller, address requiredCaller); // unauthorized caller attempted restricted action + + /* ─────────────────────────── Initialization ─────────────────────────── */ + + /** + * @notice Initialize the SymmUnlockManager with core contracts and configuration. + * @param _symmBuildersNft Address of the SymmioBuildersNFT contract. + * @param _symm Address of the SYMM token contract. + * @param _vestingContract Address of the Vesting contract. + * @param _admin Address to receive admin and all role assignments. + * @param _cliffDuration Duration of the cliff period in seconds. + * @param _vestingDuration Duration of the vesting period in seconds. + * + * @dev Sets up access control and validates all inputs. Reverts on zero addresses or invalid durations. + */ + constructor(address _symmBuildersNft, address _symm, address _vestingContract, address _admin, uint256 _cliffDuration, uint256 _vestingDuration) { + if (_symmBuildersNft == address(0) || _symm == address(0) || _vestingContract == address(0) || _admin == address(0)) { + revert ZeroAddress(); + } + if (_cliffDuration == 0 || _vestingDuration == 0) { + revert InvalidDuration(); + } + + symmBuildersNft = ISymmioBuildersNft(_symmBuildersNft); + SYMM = IERC20(_symm); + vestingContract = IVesting(_vestingContract); + cliffDuration = _cliffDuration; + vestingDuration = _vestingDuration; + + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(SETTER_ROLE, _admin); + _grantRole(PAUSER_ROLE, _admin); + _grantRole(UNPAUSER_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(); + } + + /* ──────────────────── Unlock Management ──────────────────── */ + + /** + * @notice Initiate an unlock request for an NFT (called by SymmioBuildersNFT contract). + * @param tokenId ID of the NFT to unlock. + * @param owner Owner of the NFT. + * @param amount Amount of tokens to unlock. + * + * @dev Creates a new unlock request with cliff period enforcement. + * Only callable by the SymmioBuildersNFT contract. + */ + function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external whenNotPaused { + if (msg.sender != address(symmBuildersNft)) { + revert UnauthorizedAccess(msg.sender, address(symmBuildersNft)); + } + if (amount == 0) { + revert ZeroAmount(); + } + + uint256 unlockId = _unlockIdCounter++; + unlockRequests[unlockId] = UnlockRequest({ + amount: amount, + unlockInitiatedTime: block.timestamp, + owner: owner, + tokenId: tokenId, + cliffPassed: false, + vestingStarted: false + }); + + tokenUnlockIds[tokenId].push(unlockId); + + emit UnlockInitiated(unlockId, tokenId, owner, 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 notifies the 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 (symmBuildersNft.ownerOf(request.tokenId) != msg.sender) { + revert NotNFTOwner(); + } + if (request.cliffPassed) { + revert CliffNotPassed(); + } + + 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; + } + } + + // Notify NFT contract to cancel the unlock + symmBuildersNft.cancelUnlock(tokenId, 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 Transfers tokens to vesting contract and sets up 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 (symmBuildersNft.ownerOf(request.tokenId) != msg.sender) { + revert NotNFTOwner(); + } + if (request.vestingStarted) { + revert VestingAlreadyStarted(); + } + if (block.timestamp < request.unlockInitiatedTime + cliffDuration) { + revert CliffNotPassed(); + } + + // Mark cliff as passed and vesting as started + request.cliffPassed = true; + request.vestingStarted = true; + + // Complete unlock on NFT contract + symmBuildersNft.completeUnlock(request.tokenId, request.amount); + + // Set up vesting plan for the owner + address[] memory users = new address[](1); + users[0] = request.owner; + uint256[] memory amounts = new uint256[](1); + amounts[0] = request.amount; + + // Approve vesting contract to transfer tokens + SYMM.approve(address(vestingContract), request.amount); + + // Set up vesting plan starting from now + vestingContract.setupVestingPlans(address(SYMM), block.timestamp, block.timestamp + vestingDuration, users, amounts); + + emit CliffCompleted(unlockId, request.tokenId, request.owner); + emit VestingStarted(unlockId, request.tokenId, request.owner, request.amount); + } + + /* ────────────────────────── Admin Functions ────────────────────────── */ + + /** + * @notice Update the cliff duration for new unlock requests. + * @param _cliffDuration New cliff duration in seconds. + * + * @dev Only callable by accounts with 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 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 Update the vesting contract address. + * @param _vestingContract New vesting contract address. + * + * @dev Only callable by accounts with SETTER_ROLE. Cannot be zero address. + */ + function setVestingContract(address _vestingContract) external onlyRole(SETTER_ROLE) { + if (_vestingContract == address(0)) { + revert ZeroAddress(); + } + vestingContract = IVesting(_vestingContract); + emit VestingContractUpdated(_vestingContract); + } + + /** + * @notice Rescue tokens accidentally sent to the contract. + * @param token Address of the token to rescue. + * @param to Recipient address for the rescued tokens. + * @param amount Amount of tokens to transfer. + * + * @dev Only callable by accounts with DEFAULT_ADMIN_ROLE for emergency recovery. + */ + function rescueTokens(address token, address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + IERC20(token).safeTransfer(to, amount); + } + + /* ────────────────────────── View Functions ────────────────────────── */ + + /** + * @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 active unlock requests for a specific NFT. + * @param tokenId ID of the NFT to query. + * @return Array of active UnlockRequest structs (excluding completed/vesting requests). + * + * @dev Filters out requests that have started vesting or been completed. + */ + function getActiveUnlockRequests(uint256 tokenId) external view returns (UnlockRequest[] memory) { + uint256[] memory unlockIds = tokenUnlockIds[tokenId]; + uint256 activeCount = 0; + + // Count active requests (non-zero amount and not vesting) + for (uint256 i = 0; i < unlockIds.length; i++) { + if (unlockRequests[unlockIds[i]].amount > 0 && !unlockRequests[unlockIds[i]].vestingStarted) { + activeCount++; + } + } + + // Populate active requests array + UnlockRequest[] memory activeRequests = new UnlockRequest[](activeCount); + uint256 index = 0; + for (uint256 i = 0; i < unlockIds.length; i++) { + UnlockRequest storage request = unlockRequests[unlockIds[i]]; + if (request.amount > 0 && !request.vestingStarted) { + activeRequests[index++] = request; + } + } + + return activeRequests; + } + + /** + * @notice Check if an NFT has any active unlock requests. + * @param tokenId ID of the NFT to check. + * @return Whether the NFT has active unlock requests. + */ + function isUnlocking(uint256 tokenId) external view returns (bool) { + uint256[] memory unlockIds = tokenUnlockIds[tokenId]; + for (uint256 i = 0; i < unlockIds.length; i++) { + UnlockRequest storage request = unlockRequests[unlockIds[i]]; + if (request.amount > 0 && !request.vestingStarted) { + return true; + } + } + return false; + } + + /** + * @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) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return 0; + } + return request.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) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return false; + } + return block.timestamp >= request.unlockInitiatedTime + cliffDuration; + } + + /** + * @notice Get the time remaining in the cliff period for an unlock request. + * @param unlockId ID of the unlock request. + * @return Seconds remaining until cliff period ends, or 0 if passed/invalid. + */ + function getCliffTimeRemaining(uint256 unlockId) external view returns (uint256) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return 0; + } + + uint256 cliffEndTime = request.unlockInitiatedTime + cliffDuration; + if (block.timestamp >= cliffEndTime) { + return 0; + } + + return cliffEndTime - block.timestamp; + } +} 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/vesting/SymmVestingPlanInitializer.sol b/contracts/vesting/SymmVestingPlanInitializer.sol index 54d4252..9e2abfa 100644 --- a/contracts/vesting/SymmVestingPlanInitializer.sol +++ b/contracts/vesting/SymmVestingPlanInitializer.sol @@ -150,7 +150,7 @@ contract SymmVestingPlanInitializer is AccessControlEnumerable, Pausable { // Penalty scales linearly: for each day, add PENALTY_PER_DAY_BP bp (1e18 = 100%) uint256 penalty = (daysElapsed * PENALTY_PER_DAY_BP) / 1e18; - uint256 endTime = VESTING_DURATION + launchDay + penalty; - return endTime; + uint256 et = VESTING_DURATION + launchDay + penalty; + return et; } } diff --git a/contracts/vesting/VestingV2.sol b/contracts/vesting/VestingV2.sol new file mode 100644 index 0000000..b9f73cd --- /dev/null +++ b/contracts/vesting/VestingV2.sol @@ -0,0 +1,521 @@ +// 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 { + _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 { + if (users.length != amounts.length) revert MismatchArrays(); + uint256 len = users.length; + // 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); + + // Increment plan count for the user + userVestingPlanCount[token][user]++; + emit VestingPlanSetup(token, user, planId, amount, startTime, endTime); + } + } + + /** + * @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/package-lock.json b/package-lock.json index 3d62580..5447b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "name": "symm", "version": "1.0.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", @@ -6065,20 +6066,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", @@ -7594,6 +7646,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/future_reset_vesting_plans.py b/scripts/future_reset_vesting_plans.py new file mode 100644 index 0000000..0da745d --- /dev/null +++ b/scripts/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/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/scripts/reset_vesting_plans.py b/scripts/reset_vesting_plans.py new file mode 100644 index 0000000..f33dd5b --- /dev/null +++ b/scripts/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/user_allocation.py b/scripts/user_allocation.py deleted file mode 100644 index fb311b1..0000000 --- a/scripts/user_allocation.py +++ /dev/null @@ -1,60 +0,0 @@ -import json -import os - -import requests -from multicallable import Multicallable -from web3 import Web3 - -SUBGRAPH_URL = "https://api.studio.thegraph.com/query/85206/allocated/version/latest" - -rpc = "https://base-rpc.publicnode.com" -rpc = "https://base.llamarpc.com" -rpc = "https://base.drpc.org" - -contract_address = "0x232b72527e3692E78d7f6D73634fc4E100E31f80" - -w3 = Web3(Web3.HTTPProvider(rpc)) - - -def fetch_all_users(): - all_users = set() - last_id = "" - while True: - query = f""" - {{ - batchAllocationsSets(orderBy: id, orderDirection: asc, first: 1000, where: {{id_gt: "{last_id}"}}) {{ - id - users - }} - }} - """ - response = requests.post(SUBGRAPH_URL, json={'query': query}) - data = response.json() - sets = data["data"]["batchAllocationsSets"] - - if not sets: - break - - for batch in sets: - for user in batch["users"]: - all_users.add(w3.to_checksum_address(user)) - - last_id = sets[-1]["id"] - - return list(all_users) - - -users = fetch_all_users() -print(f"Fetched {len(users)} unique users.") - -with open(f"{os.getcwd()}/abis/SymmAllocationClaimer.json", "r") as f: - abi = json.load(f) - -contract = Multicallable(w3.to_checksum_address(contract_address), abi, w3) -available = contract.userAllocations(users).call(n=len(users) // 200 + 1, progress_bar=True) -rows = {"Users": [], "Available": []} -for user, amount in zip(users, available): - rows["Users"].append(user) - rows["Available"].append(str(amount)) -with open(f"{os.getcwd()}/user_available_symm.json", "w") as f: - f.write(json.dumps(rows, indent=2)) diff --git a/scripts/vesting_setup.py b/scripts/vesting_setup.py new file mode 100644 index 0000000..ed4373c --- /dev/null +++ b/scripts/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) From 953a870d6165926663312e04d6290d70a6ed9d6d Mon Sep 17 00:00:00 2001 From: Naveed Date: Mon, 14 Jul 2025 11:52:18 +0200 Subject: [PATCH 24/48] Make nft contracts upgradable --- contracts/builders-nft/SymmioBuildersNft.sol | 65 +++++++++++++++---- .../builders-nft/SymmioUnlockManager.sol | 56 +++++++++++++--- 2 files changed, 99 insertions(+), 22 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index 32c73da..56326f1 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -2,8 +2,8 @@ pragma solidity ^0.8.27; /** - * @title SymmioBuildersNft - * @notice Advanced ERC721 NFT contract for locking SYMM tokens on the Base chain to enable + * @title SymmioBuildersNftUpgradeable + * @notice Upgradeable version of the advanced ERC721 NFT contract for locking SYMM tokens on the Base chain to enable * fee reductions across multiple chains. Each NFT represents a locked amount of SYMM * tokens with customizable branding and comprehensive unlock management capabilities. * @@ -17,18 +17,20 @@ pragma solidity ^0.8.27; * • Granular pause controls for transfers and contract operations * • Role-based access control for administrative and sync functions * • Comprehensive view functions for user and system queries + * • TransparentUpgradeableProxy pattern for secure upgrades * * Integration points include external unlock manager for time-locked releases, * fee collector contracts for cross-chain fee reduction tracking, and sync * mechanisms for maintaining consistency across multiple blockchain networks. */ -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; /* ────────────────────────── External Interfaces ────────────────────────── */ @@ -69,7 +71,13 @@ interface ISymmFeeCollector { function onLockedAmountChanged(int256 amount) external; } -contract SymmioBuildersNft is ERC721Enumerable, AccessControlEnumerable, Pausable, ReentrancyGuard { +contract SymmioBuildersNftUpgradeable is + Initializable, + ERC721EnumerableUpgradeable, + AccessControlEnumerableUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable +{ using SafeERC20 for IERC20; /* ─────────────────────────────── Roles ─────────────────────────────── */ @@ -91,8 +99,8 @@ contract SymmioBuildersNft is ERC721Enumerable, AccessControlEnumerable, Pausabl /* ──────────────────────── Storage Variables ──────────────────────── */ - /// @notice The SYMM token contract address (immutable for gas efficiency). - IERC20Burnable public immutable SYMM; + /// @notice The SYMM token contract address. + IERC20Burnable public SYMM; /// @notice The unlock manager contract for handling token unlock processes. ISymmUnlockManager public unlockManager; @@ -115,6 +123,9 @@ contract SymmioBuildersNft is ERC721Enumerable, AccessControlEnumerable, Pausabl /// @notice Mapping of token ID to its related fee collector addresses for fee reduction tracking. mapping(uint256 => address[]) public tokenRelatedFeeCollectors; + /// @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 ─────────────────────────────── */ /** @@ -222,18 +233,32 @@ contract SymmioBuildersNft is ERC721Enumerable, AccessControlEnumerable, Pausabl /* ─────────────────────────── Initialization ─────────────────────────── */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /** - * @notice Initialize the SymmioBuildersNft contract with core parameters. + * @notice Initialize the upgradeable SymmioBuildersNft contract with core parameters. * @param _symm Address of the SYMM token contract. * @param _admin Address to receive admin and all role assignments. * @param _minLockAmount Minimum amount of SYMM tokens required to mint an NFT. * * @dev Sets up the ERC721 contract, assigns comprehensive roles, and validates inputs. + * This function replaces the constructor for upgradeable contracts. */ - constructor(address _symm, address _admin, uint256 _minLockAmount) ERC721("Symmio Builders NFT", "BUILDERS") { + function initialize(address _symm, address _admin, uint256 _minLockAmount) public initializer { if (_symm == address(0) || _admin == address(0)) revert ZeroAddress(); if (_minLockAmount == 0) revert ZeroAmount(); + // Initialize parent contracts + __ERC721_init("Symmio Builders NFT", "BUILDERS"); + __ERC721Enumerable_init(); + __AccessControlEnumerable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + // Set contract-specific state SYMM = IERC20Burnable(_symm); minLockAmount = _minLockAmount; @@ -561,6 +586,18 @@ contract SymmioBuildersNft is ERC721Enumerable, AccessControlEnumerable, Pausabl emit FeeCollectorRemoved(tokenId, feeCollector); } + /* ────────────────────────── Version & Info ────────────────────────── */ + + /** + * @notice Get the current contract version. + * @return The version string of the current contract. + * + * @dev This can be overridden in future upgrades to track versions. + */ + function version() external pure returns (string memory) { + return "1.0.0"; + } + /* ────────────────────────── View Functions ────────────────────────── */ /** @@ -646,9 +683,11 @@ contract SymmioBuildersNft is ERC721Enumerable, AccessControlEnumerable, Pausabl * @param interfaceId Interface ID to check. * @return Whether the interface is supported. * - * @dev Supports both ERC721Enumerable and AccessControlEnumerable interfaces. + * @dev Supports ERC721Enumerable and AccessControlEnumerable interfaces. */ - function supportsInterface(bytes4 interfaceId) public view override(ERC721Enumerable, AccessControlEnumerable) returns (bool) { + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable) returns (bool) { return super.supportsInterface(interfaceId); } } diff --git a/contracts/builders-nft/SymmioUnlockManager.sol b/contracts/builders-nft/SymmioUnlockManager.sol index a1bf263..85bf4af 100644 --- a/contracts/builders-nft/SymmioUnlockManager.sol +++ b/contracts/builders-nft/SymmioUnlockManager.sol @@ -21,13 +21,16 @@ pragma solidity ^0.8.27; * The contract coordinates between SymmioBuildersNFT for lock management and external * vesting contracts for token distribution, ensuring secure and controlled token unlocking * with configurable time-based restrictions and user flexibility. + * + * @dev This contract is designed to be used with OpenZeppelin's TransparentUpgradeableProxy. */ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/access/extensions/AccessControlEnumerable.sol"; -import "@openzeppelin/contracts/utils/Pausable.sol"; -import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /* ────────────────────────── External Interfaces ────────────────────────── */ @@ -84,7 +87,7 @@ interface IVesting { function setupVestingPlans(address token, uint256 startTime, uint256 endTime, address[] memory users, uint256[] memory amounts) external; } -contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard { +contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; /* ─────────────────────────────── Roles ─────────────────────────────── */ @@ -100,14 +103,14 @@ contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard /* ──────────────────────── Storage Variables ──────────────────────── */ - /// @notice The SymmioBuildersNFT contract (immutable for gas optimization). - ISymmioBuildersNft public immutable symmBuildersNft; + /// @notice The SymmioBuildersNFT contract. + ISymmioBuildersNft public symmBuildersNft; /// @notice The Vesting contract for managing token vesting plans. IVesting public vestingContract; - /// @notice The SYMM token contract (immutable for gas optimization). - IERC20 public immutable SYMM; + /// @notice The SYMM token contract. + IERC20 public SYMM; /// @notice Duration of the cliff period in seconds before tokens can be unlocked. uint256 public cliffDuration; @@ -124,6 +127,9 @@ contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard /// @notice Mapping of NFT token ID to array of associated unlock request IDs. mapping(uint256 => uint256[]) public tokenUnlockIds; + /// @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; + /* ─────────────────────────────── Structs ─────────────────────────────── */ /** @@ -213,6 +219,11 @@ contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard /* ─────────────────────────── Initialization ─────────────────────────── */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + /** * @notice Initialize the SymmUnlockManager with core contracts and configuration. * @param _symmBuildersNft Address of the SymmioBuildersNFT contract. @@ -223,8 +234,16 @@ contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard * @param _vestingDuration Duration of the vesting period in seconds. * * @dev Sets up access control and validates all inputs. Reverts on zero addresses or invalid durations. + * This replaces the constructor for upgradeable contracts. */ - constructor(address _symmBuildersNft, address _symm, address _vestingContract, address _admin, uint256 _cliffDuration, uint256 _vestingDuration) { + function initialize( + address _symmBuildersNft, + address _symm, + address _vestingContract, + address _admin, + uint256 _cliffDuration, + uint256 _vestingDuration + ) public initializer { if (_symmBuildersNft == address(0) || _symm == address(0) || _vestingContract == address(0) || _admin == address(0)) { revert ZeroAddress(); } @@ -232,12 +251,22 @@ contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard revert InvalidDuration(); } + // Initialize parent contracts + __AccessControlEnumerable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + // Set contract addresses and parameters symmBuildersNft = ISymmioBuildersNft(_symmBuildersNft); SYMM = IERC20(_symm); vestingContract = IVesting(_vestingContract); cliffDuration = _cliffDuration; vestingDuration = _vestingDuration; + // Initialize counter + _unlockIdCounter = 0; + + // Grant roles to admin _grantRole(DEFAULT_ADMIN_ROLE, _admin); _grantRole(SETTER_ROLE, _admin); _grantRole(PAUSER_ROLE, _admin); @@ -541,4 +570,13 @@ contract SymmUnlockManager is AccessControlEnumerable, Pausable, ReentrancyGuard return cliffEndTime - block.timestamp; } + + /** + * @notice Returns the current version of the contract. + * @return Version string of the contract. + * @dev This function can be used to verify which version of the contract is deployed. + */ + function version() external pure returns (string memory) { + return "1.0.0"; + } } From c03e79ecbcc8aca1115c4008e040933da3c73d14 Mon Sep 17 00:00:00 2001 From: timaster Date: Mon, 21 Jul 2025 10:31:59 +0200 Subject: [PATCH 25/48] Add VestingUpsertManager --- .gitignore | 2 +- contracts/vesting/VestingUpsertManager.sol | 87 ++++++++++++++++++++++ hardhat.config.ts | 4 +- package-lock.json | 3 +- scripts/deploy_vesting_upsert_manager.ts | 47 ++++++++++++ scripts/vesting_upsert_manager.py | 74 ++++++++++++++++++ 6 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 contracts/vesting/VestingUpsertManager.sol create mode 100644 scripts/deploy_vesting_upsert_manager.ts create mode 100644 scripts/vesting_upsert_manager.py diff --git a/.gitignore b/.gitignore index 088cef4..a46e3dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules -.env +.env* # Hardhat files /cache diff --git a/contracts/vesting/VestingUpsertManager.sol b/contracts/vesting/VestingUpsertManager.sol new file mode 100644 index 0000000..0097cc4 --- /dev/null +++ b/contracts/vesting/VestingUpsertManager.sol @@ -0,0 +1,87 @@ +import "./interfaces/IVesting.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "hardhat/console.sol"; + +interface ISymmVestingPlanInitializer { + function endTimeStartsAt(uint256 _timestamp) external view returns (uint256); +} + +contract VestingUpsertManager is AccessControl { + IVesting public vesting; + ISymmVestingPlanInitializer public initializer; + + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + constructor(address admin, address vestingAddress, address vestingPlanAddress) { + vesting = IVesting(vestingAddress); + initializer = ISymmVestingPlanInitializer(vestingPlanAddress); + _grantRole(OPERATOR_ROLE, admin); + } + + function upsertVestingPlans( + address token, + address[] calldata users, + uint256[] calldata newAmounts + ) external onlyRole(OPERATOR_ROLE) { + require(users.length == newAmounts.length, "Length mismatch"); + uint256 startTime = block.timestamp; + uint256 endTime = initializer.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(token, users[i]); + toReset[resetCount] = users[i]; + toResetAmounts[resetCount] = locked + newAmounts[i]; + resetCount++; + } + } + console.log("setup:", setupCount); + console.log("reset:", 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/hardhat.config.ts b/hardhat.config.ts index b131cfa..f9be756 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -44,8 +44,8 @@ export const config: HardhatUserConfig = { networks: { hardhat: { forking: { - url: "https://base.drpc.org", - blockNumber: 29871098, + url: "https://1rpc.io/base", + blockNumber: 33113717, }, }, ethereum: { diff --git a/package-lock.json b/package-lock.json index 5447b6b..a542278 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3461,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", diff --git a/scripts/deploy_vesting_upsert_manager.ts b/scripts/deploy_vesting_upsert_manager.ts new file mode 100644 index 0000000..2dc41fd --- /dev/null +++ b/scripts/deploy_vesting_upsert_manager.ts @@ -0,0 +1,47 @@ +// scripts/deploy.ts +import { ethers, network } from "hardhat"; + +async function main() { + const [deployer] = await ethers.getSigners(); + + console.log("Deploying contracts with:", deployer.address); + + const admin = deployer.address; + const vestingAddress = "0x5733105364c8136226e246455328884c23151C60"; + const initializerAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15"; + const defaultAdmin = "0x8CF65060CdA270a3886452A1A1cb656BECEE5bA4"; + const account1 = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + + const vestingContract = await ethers.getContractAt("Vesting", vestingAddress); + const vestingPlanContract = await ethers.getContractAt("SymmVestingPlanInitializer", initializerAddress); + + const VestingUpsertManager = await ethers.getContractFactory("VestingUpsertManager"); + const upsertManager = await VestingUpsertManager.deploy(admin, vestingAddress, initializerAddress); + + await upsertManager.waitForDeployment(); + + console.log("VestingUpsertManager deployed to:", await upsertManager.getAddress()); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [defaultAdmin], + }); + + await deployer.sendTransaction({ + to: defaultAdmin, + value: ethers.parseEther("1") + }) + + const signer = await ethers.getSigner(defaultAdmin); + const OPERATOR_ROLE = await upsertManager.OPERATOR_ROLE(); + const SETTER_ROLE = await vestingContract.SETTER_ROLE(); + + await vestingContract.connect(signer).grantRole(OPERATOR_ROLE, account1); + await vestingContract.connect(signer).grantRole(SETTER_ROLE, await upsertManager.getAddress()); + await vestingPlanContract.connect(signer).grantRole(OPERATOR_ROLE, account1); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/vesting_upsert_manager.py b/scripts/vesting_upsert_manager.py new file mode 100644 index 0000000..8a44431 --- /dev/null +++ b/scripts/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") From d7f5505be3d2390e10a5c10539fbb7c92b282d02 Mon Sep 17 00:00:00 2001 From: timaster Date: Mon, 21 Jul 2025 10:31:59 +0200 Subject: [PATCH 26/48] Add VestingV2 and tests --- contracts/vesting/SymmVestingV2.sol | 270 ++++++++++ contracts/vesting/VestingV2.sol | 29 +- tasks/index.ts | 3 +- tasks/symmStaking.ts | 11 +- tasks/symmVesting.ts | 11 +- tasks/symmVestingPlanInitializer.ts | 2 +- tasks/symmVestingV2.ts | 134 +++++ tests/Initialize.fixture.ts | 38 +- tests/main.ts | 9 +- tests/vestingV2.behavior.ts | 742 ++++++++++++++++++++++++++++ 10 files changed, 1217 insertions(+), 32 deletions(-) create mode 100644 contracts/vesting/SymmVestingV2.sol create mode 100644 tasks/symmVestingV2.ts create mode 100644 tests/vestingV2.behavior.ts 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/VestingV2.sol b/contracts/vesting/VestingV2.sol index b9f73cd..6dcaa61 100644 --- a/contracts/vesting/VestingV2.sol +++ b/contracts/vesting/VestingV2.sol @@ -23,13 +23,13 @@ pragma solidity >=0.8.18; * 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"; +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; @@ -185,8 +185,8 @@ contract VestingV2 is Initializable, AccessControlEnumerableUpgradeable, Pausabl uint256 endTime, address[] memory users, uint256[] memory amounts - ) external onlyRole(SETTER_ROLE) whenNotPaused nonReentrant { - _setupVestingPlans(token, startTime, endTime, users, amounts); + ) external onlyRole(SETTER_ROLE) whenNotPaused nonReentrant returns (uint256[] memory) { + return _setupVestingPlans(token, startTime, endTime, users, amounts); } /** @@ -308,9 +308,16 @@ contract VestingV2 is Initializable, AccessControlEnumerableUpgradeable, Pausabl * * @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 { + 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]; @@ -322,10 +329,12 @@ contract VestingV2 is Initializable, AccessControlEnumerableUpgradeable, Pausabl 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; } /** diff --git a/tasks/index.ts b/tasks/index.ts index 5f0d94d..8c1a2f3 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -3,4 +3,5 @@ import "./symmioToken" import "./symmStaking" import "./symmAllocationClaimer" import "./symmVesting" -import "./symmVestingPlanInitializerSetup" \ No newline at end of file +import "./symmVestingPlanInitializerSetup" +import "./symmVestingV2" \ No newline at end of file diff --git a/tasks/symmStaking.ts b/tasks/symmStaking.ts index c845fe4..9afa255 100644 --- a/tasks/symmStaking.ts +++ b/tasks/symmStaking.ts @@ -1,6 +1,6 @@ import { task, types } from "hardhat/config" -task("deploy:staking", "Deploys the SymmStaking logic and proxy using CREATE2") +task("deploy:SymmStaking", "Deploys the SymmStaking logic and proxy using CREATE2") .addParam("admin", "The admin of the SymmStaking contract") .addParam("token", "The address of the staking token") .addParam("factory", "The deployed Create2Factory contract address") @@ -79,8 +79,9 @@ task("deploy:staking", "Deploys the SymmStaking logic and proxy using CREATE2") console.log("Deterministic TransparentUpgradeableProxy deployed at:", predictedProxyAddress) } - return { - implementation: predictedImplAddress, - proxy: predictedProxyAddress, - } + // return { + // implementation: predictedImplAddress, + // proxy: predictedProxyAddress, + // } + return await ethers.getContractAt("SymmStaking", predictedProxyAddress) }) diff --git a/tasks/symmVesting.ts b/tasks/symmVesting.ts index 836b2ba..ec9a66a 100644 --- a/tasks/symmVesting.ts +++ b/tasks/symmVesting.ts @@ -125,9 +125,10 @@ task("deploy:vesting", "Deploys the SymmVesting logic and proxy using CREATE2") console.log("Deterministic TransparentUpgradeableProxy deployed at:", predictedProxyAddress) } - return { - library: predictedLibAddress, - implementation: predictedImplAddress, - proxy: predictedProxyAddress, - } + // return { + // library: predictedLibAddress, + // implementation: predictedImplAddress, + // proxy: predictedProxyAddress, + // } + return await ethers.getContractAt("SymmVesting", predictedProxyAddress) }) diff --git a/tasks/symmVestingPlanInitializer.ts b/tasks/symmVestingPlanInitializer.ts index f764764..8db24d6 100644 --- a/tasks/symmVestingPlanInitializer.ts +++ b/tasks/symmVestingPlanInitializer.ts @@ -27,7 +27,7 @@ task("deploy:SymmVestingPlanInitializer", "Deploys the SymmVestingPlanInitialize Available: string[] } = JSON.parse(data); - const chunkSize = 1000; + const chunkSize = 100; const users = user_available.Users; const amounts = user_available.Available; diff --git a/tasks/symmVestingV2.ts b/tasks/symmVestingV2.ts new file mode 100644 index 0000000..6b4faee --- /dev/null +++ b/tasks/symmVestingV2.ts @@ -0,0 +1,134 @@ +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 + + // 1. Deploy the VestingPlanOps library first + console.log("Deploying VestingPlanOps library...") + const VestingPlanOpsFactory = await ethers.getContractFactory("VestingPlanOps") + + // 2. Get an instance of your Create2Factory contract + const create2Factory = await ethers.getContractAt("Create2Factory", factory) + + // 3. Prepare library deployment bytecode + const libDeployTx = await VestingPlanOpsFactory.getDeployTransaction() + const libBytecode = libDeployTx.data + if (!libBytecode) { + throw new Error("Cannot obtain library deployment bytecode") + } + + // 4. Compute a deterministic salt for library + const librarySalt = ethers.keccak256(ethers.toUtf8Bytes(`library-vesting-planops`)) + console.log("Library salt:", librarySalt) + + // 5. Compute the predicted library address + const predictedLibAddress = await create2Factory.getFunction("getAddress")(libBytecode, librarySalt) + console.log("Predicted library address:", predictedLibAddress) + + if (!dryRun) { + // 6. Deploy the library via the factory using CREATE2 + console.log("Deploying library via CREATE2...") + const libTx = await create2Factory.deploy(libBytecode, librarySalt) + await libTx.wait() + console.log("Library deployed at:", predictedLibAddress) + } + + console.log() + + // 7. Get the contract factory for the logic contract with library linkage + const SymmVestingFactory = await ethers.getContractFactory("SymmVestingV2", { + libraries: { + VestingPlanOps: predictedLibAddress, + }, + }) + + // 8. Prepare implementation deployment bytecode + const implDeployTx = await SymmVestingFactory.getDeployTransaction() + const implBytecode = implDeployTx.data + if (!implBytecode) { + throw new Error("Cannot obtain implementation deployment bytecode") + } + + // 9. Compute a deterministic salt for implementation if not provided + const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vestingV2`)) + console.log("Implementation salt:", implementationSalt) + + // 10. Compute the predicted implementation address + const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt) + console.log("Predicted implementation address:", predictedImplAddress) + + if (!dryRun) { + // 11. 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() + } + + // 12. 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) + + // 13. 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") + } + + // 15. 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) + + // 16. Compute the predicted proxy address + const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue) + console.log("Predicted proxy address:", predictedProxyAddress) + + if (!dryRun) { + // 17. 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 { + // library: predictedLibAddress, + // implementation: predictedImplAddress, + // proxy: predictedProxyAddress, + // } + return await ethers.getContractAt("SymmVestingV2", predictedProxyAddress) + }) diff --git a/tests/Initialize.fixture.ts b/tests/Initialize.fixture.ts index 40e6ceb..71eb076 100644 --- a/tests/Initialize.fixture.ts +++ b/tests/Initialize.fixture.ts @@ -1,7 +1,14 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" import { ethers, run } from "hardhat" import { e } from "../utils" -import { SymmAllocationClaimer, Symmio, Vesting, SymmStaking, SymmVestingPlanInitializer } from "../typechain-types" +import { + SymmAllocationClaimer, + Symmio, + Vesting, + SymmStaking, + SymmVestingPlanInitializer, + VestingV2, +} from "../typechain-types"; import * as Process from "process" import { time } from "@nomicfoundation/hardhat-network-helpers" import { floor } from "lodash" @@ -18,7 +25,7 @@ export class RunContext { } symmioToken!: Symmio claimSymm!: SymmAllocationClaimer - vesting!: Vesting + vesting!: VestingV2 symmStaking!: SymmStaking symmVestingVlanInitializer!: SymmVestingPlanInitializer } @@ -50,7 +57,22 @@ export async function initializeFixture(): Promise { mintFactor: "500000000000000000", //5e17 => %50 }) - context.vesting = await run("deploy:vesting", { + // 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.vesting = await run("deploy:vestingV2", { admin: await context.signers.admin.getAddress(), penaltyreceiver: await context.signers.vestingPenaltyReceiver.getAddress(), pool: Process.env.POOL, @@ -61,20 +83,20 @@ export async function initializeFixture(): Promise { usdc: Process.env.USDC, lp: Process.env.SYMM_LP, factory: Process.env.FACTORY, - implsalt: "A", - proxysalt: "B", + implsalt: "1", + proxysalt: "2", }) context.symmStaking = await run("deploy:SymmStaking", { admin: await context.signers.admin.getAddress(), - stakingToken: await context.symmioToken.getAddress(), + token: await context.symmioToken.getAddress(), + factory: Process.env.FACTORY, }) context.symmVestingVlanInitializer = await run("deploy:SymmVestingPlanInitializer", { - admin: await context.signers.admin.getAddress(), symmTokenAddress: await context.symmioToken.getAddress(), symmVestingAddress: await context.vesting.getAddress(), - totalInitiatableSYMM: "10000000000000000000000000", //10Me18 + totalInitiatableSYMM: "1000000000000000000000000000", //10Me18 launchTimeStamp: String(floor(Date.now() / 1000) + 7 * 24 * 60 * 60), }) diff --git a/tests/main.ts b/tests/main.ts index b69dc5e..19e9fae 100644 --- a/tests/main.ts +++ b/tests/main.ts @@ -4,6 +4,7 @@ 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"; describe("Symmio Token", () => { // if (process.env.TEST_MODE === "static") { @@ -28,8 +29,12 @@ describe("Symmio Token", () => { // ShouldBehaveLikeVesting() // }) - describe("Symm Vesting Plan Initializer", async function () { - shouldBehaveLikeSymmVestingPlanInitializer() + // describe("Symm Vesting Plan Initializer", async function () { + // shouldBehaveLikeSymmVestingPlanInitializer() + // }) + + describe("Vesting V2", async function () { + ShouldBehaveLikeVestingV2() }) }) // } else if (process.env.TEST_MODE === "dynamic") { 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])); + // } + // }); + // }); +} From f1150659b0897cbbc13c436ec55b1f4efb9a5d39 Mon Sep 17 00:00:00 2001 From: timaster Date: Mon, 21 Jul 2025 10:31:59 +0200 Subject: [PATCH 27/48] Fix VestingUpsertManager to upgradeable --- contracts/vesting/VestingUpsertManager.sol | 21 +++++---- scripts/deploy_vesting_upsert_manager.ts | 41 +++++----------- .../local_deploy_vesting_upsert_manager.ts | 47 +++++++++++++++++++ 3 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 scripts/local_deploy_vesting_upsert_manager.ts diff --git a/contracts/vesting/VestingUpsertManager.sol b/contracts/vesting/VestingUpsertManager.sol index 0097cc4..b403a72 100644 --- a/contracts/vesting/VestingUpsertManager.sol +++ b/contracts/vesting/VestingUpsertManager.sol @@ -1,20 +1,25 @@ import "./interfaces/IVesting.sol"; -import "@openzeppelin/contracts/access/AccessControl.sol"; -import "hardhat/console.sol"; +import { AccessControlEnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; interface ISymmVestingPlanInitializer { function endTimeStartsAt(uint256 _timestamp) external view returns (uint256); } -contract VestingUpsertManager is AccessControl { +contract VestingUpsertManager is Initializable, AccessControlEnumerableUpgradeable { IVesting public vesting; - ISymmVestingPlanInitializer public initializer; + ISymmVestingPlanInitializer public planInitializer; bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); - constructor(address admin, address vestingAddress, address vestingPlanAddress) { + function initialize( + address admin, + address vestingAddress, + address vestingPlanAddress + ) public initializer { + __AccessControl_init(); vesting = IVesting(vestingAddress); - initializer = ISymmVestingPlanInitializer(vestingPlanAddress); + planInitializer = ISymmVestingPlanInitializer(vestingPlanAddress); _grantRole(OPERATOR_ROLE, admin); } @@ -25,7 +30,7 @@ contract VestingUpsertManager is AccessControl { ) external onlyRole(OPERATOR_ROLE) { require(users.length == newAmounts.length, "Length mismatch"); uint256 startTime = block.timestamp; - uint256 endTime = initializer.endTimeStartsAt(startTime); + uint256 endTime = planInitializer.endTimeStartsAt(startTime); address[] memory toSetup = new address[](users.length); uint256[] memory toSetupAmounts = new uint256[](users.length); @@ -49,8 +54,6 @@ contract VestingUpsertManager is AccessControl { resetCount++; } } - console.log("setup:", setupCount); - console.log("reset:", resetCount); if (setupCount > 0) { vesting.setupVestingPlans( diff --git a/scripts/deploy_vesting_upsert_manager.ts b/scripts/deploy_vesting_upsert_manager.ts index 2dc41fd..7702c0b 100644 --- a/scripts/deploy_vesting_upsert_manager.ts +++ b/scripts/deploy_vesting_upsert_manager.ts @@ -1,44 +1,27 @@ -// scripts/deploy.ts -import { ethers, network } from "hardhat"; +const { ethers, upgrades } = require("hardhat"); async function main() { const [deployer] = await ethers.getSigners(); - console.log("Deploying contracts with:", deployer.address); + console.log("Deploying with account:", deployer.address); const admin = deployer.address; const vestingAddress = "0x5733105364c8136226e246455328884c23151C60"; - const initializerAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15"; - const defaultAdmin = "0x8CF65060CdA270a3886452A1A1cb656BECEE5bA4"; - const account1 = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; - - const vestingContract = await ethers.getContractAt("Vesting", vestingAddress); - const vestingPlanContract = await ethers.getContractAt("SymmVestingPlanInitializer", initializerAddress); + const vestingPlanAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15"; const VestingUpsertManager = await ethers.getContractFactory("VestingUpsertManager"); - const upsertManager = await VestingUpsertManager.deploy(admin, vestingAddress, initializerAddress); - - await upsertManager.waitForDeployment(); - - console.log("VestingUpsertManager deployed to:", await upsertManager.getAddress()); - - await network.provider.request({ - method: "hardhat_impersonateAccount", - params: [defaultAdmin], - }); - await deployer.sendTransaction({ - to: defaultAdmin, - value: ethers.parseEther("1") - }) + const proxy = await upgrades.deployProxy( + VestingUpsertManager, + [admin, vestingAddress, vestingPlanAddress], + { + initializer: "initialize", + }, + ); - const signer = await ethers.getSigner(defaultAdmin); - const OPERATOR_ROLE = await upsertManager.OPERATOR_ROLE(); - const SETTER_ROLE = await vestingContract.SETTER_ROLE(); + await proxy.deployed(); - await vestingContract.connect(signer).grantRole(OPERATOR_ROLE, account1); - await vestingContract.connect(signer).grantRole(SETTER_ROLE, await upsertManager.getAddress()); - await vestingPlanContract.connect(signer).grantRole(OPERATOR_ROLE, account1); + console.log("VestingUpsertManager proxy deployed to:", proxy.address); } main().catch((error) => { diff --git a/scripts/local_deploy_vesting_upsert_manager.ts b/scripts/local_deploy_vesting_upsert_manager.ts new file mode 100644 index 0000000..2dc41fd --- /dev/null +++ b/scripts/local_deploy_vesting_upsert_manager.ts @@ -0,0 +1,47 @@ +// scripts/deploy.ts +import { ethers, network } from "hardhat"; + +async function main() { + const [deployer] = await ethers.getSigners(); + + console.log("Deploying contracts with:", deployer.address); + + const admin = deployer.address; + const vestingAddress = "0x5733105364c8136226e246455328884c23151C60"; + const initializerAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15"; + const defaultAdmin = "0x8CF65060CdA270a3886452A1A1cb656BECEE5bA4"; + const account1 = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + + const vestingContract = await ethers.getContractAt("Vesting", vestingAddress); + const vestingPlanContract = await ethers.getContractAt("SymmVestingPlanInitializer", initializerAddress); + + const VestingUpsertManager = await ethers.getContractFactory("VestingUpsertManager"); + const upsertManager = await VestingUpsertManager.deploy(admin, vestingAddress, initializerAddress); + + await upsertManager.waitForDeployment(); + + console.log("VestingUpsertManager deployed to:", await upsertManager.getAddress()); + + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [defaultAdmin], + }); + + await deployer.sendTransaction({ + to: defaultAdmin, + value: ethers.parseEther("1") + }) + + const signer = await ethers.getSigner(defaultAdmin); + const OPERATOR_ROLE = await upsertManager.OPERATOR_ROLE(); + const SETTER_ROLE = await vestingContract.SETTER_ROLE(); + + await vestingContract.connect(signer).grantRole(OPERATOR_ROLE, account1); + await vestingContract.connect(signer).grantRole(SETTER_ROLE, await upsertManager.getAddress()); + await vestingPlanContract.connect(signer).grantRole(OPERATOR_ROLE, account1); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); From e1ca3d2f20f8757a4201c31a411c9967b6f9192e Mon Sep 17 00:00:00 2001 From: Naveed Date: Mon, 21 Jul 2025 14:15:04 +0200 Subject: [PATCH 28/48] Fix some problems in upsert manager --- contracts/vesting/VestingUpsertManager.sol | 57 ++++++++-------------- scripts/deploy_vesting_upsert_manager.ts | 42 ++++++++-------- 2 files changed, 39 insertions(+), 60 deletions(-) diff --git a/contracts/vesting/VestingUpsertManager.sol b/contracts/vesting/VestingUpsertManager.sol index b403a72..f86320c 100644 --- a/contracts/vesting/VestingUpsertManager.sol +++ b/contracts/vesting/VestingUpsertManager.sol @@ -1,34 +1,32 @@ +// 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"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.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; - bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); - - function initialize( - address admin, - address vestingAddress, - address vestingPlanAddress - ) public initializer { - __AccessControl_init(); + function initialize(address admin, address operator, address vestingAddress, address vestingPlanAddress) public initializer { + __AccessControlEnumerable_init(); vesting = IVesting(vestingAddress); planInitializer = ISymmVestingPlanInitializer(vestingPlanAddress); - _grantRole(OPERATOR_ROLE, admin); + _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, "Length mismatch"); + 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); @@ -41,7 +39,7 @@ contract VestingUpsertManager is Initializable, AccessControlEnumerableUpgradeab uint256 resetCount = 0; for (uint256 i = 0; i < users.length; i++) { - (uint256 existingAmount,,,) = vesting.vestingPlans(token, users[i]); + (uint256 existingAmount, , , ) = vesting.vestingPlans(token, users[i]); if (existingAmount == 0) { toSetup[setupCount] = users[i]; @@ -55,36 +53,19 @@ contract VestingUpsertManager is Initializable, AccessControlEnumerableUpgradeab } } - 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) - ); - } + 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]; - } + 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]; - } + for (uint256 i = 0; i < length; i++) output[i] = input[i]; return output; } } diff --git a/scripts/deploy_vesting_upsert_manager.ts b/scripts/deploy_vesting_upsert_manager.ts index 7702c0b..c3a4d26 100644 --- a/scripts/deploy_vesting_upsert_manager.ts +++ b/scripts/deploy_vesting_upsert_manager.ts @@ -1,30 +1,28 @@ -const { ethers, upgrades } = require("hardhat"); +const { ethers, upgrades } = require("hardhat") async function main() { - const [deployer] = await ethers.getSigners(); + const [deployer] = await ethers.getSigners() - console.log("Deploying with account:", deployer.address); + console.log("Deploying with account:", deployer.address) - const admin = deployer.address; - const vestingAddress = "0x5733105364c8136226e246455328884c23151C60"; - const vestingPlanAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15"; + const admin = "" + const operator = "" + const vestingAddress = "0x5733105364c8136226e246455328884c23151C60" + const vestingPlanAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15" - const VestingUpsertManager = await ethers.getContractFactory("VestingUpsertManager"); + const Factory = await ethers.getContractFactory("VestingUpsertManager") + const contract = await upgrades.deployProxy(Factory, [admin, operator, vestingAddress, vestingPlanAddress], { initializer: "initialize" }) + await contract.waitForDeployment() - const proxy = await upgrades.deployProxy( - VestingUpsertManager, - [admin, vestingAddress, vestingPlanAddress], - { - initializer: "initialize", - }, - ); - - await proxy.deployed(); - - console.log("VestingUpsertManager proxy deployed to:", proxy.address); + 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; -}); +main().catch(error => { + console.error(error) + process.exitCode = 1 +}) From 22133e0fbf6de8065e1f56361c0015b4b1224bf4 Mon Sep 17 00:00:00 2001 From: Naveed Date: Mon, 21 Jul 2025 14:34:27 +0200 Subject: [PATCH 29/48] Clean up scripts --- ...nager.ts => deployVestingUpsertManager.ts} | 0 .../local_deploy_vesting_upsert_manager.ts | 47 ------------------- .../{ => py}/future_reset_vesting_plans.py | 0 scripts/{ => py}/reset_vesting_plans.py | 0 scripts/{ => py}/vesting_setup.py | 0 scripts/{ => py}/vesting_upsert_manager.py | 0 6 files changed, 47 deletions(-) rename scripts/{deploy_vesting_upsert_manager.ts => deployVestingUpsertManager.ts} (100%) delete mode 100644 scripts/local_deploy_vesting_upsert_manager.ts rename scripts/{ => py}/future_reset_vesting_plans.py (100%) rename scripts/{ => py}/reset_vesting_plans.py (100%) rename scripts/{ => py}/vesting_setup.py (100%) rename scripts/{ => py}/vesting_upsert_manager.py (100%) diff --git a/scripts/deploy_vesting_upsert_manager.ts b/scripts/deployVestingUpsertManager.ts similarity index 100% rename from scripts/deploy_vesting_upsert_manager.ts rename to scripts/deployVestingUpsertManager.ts diff --git a/scripts/local_deploy_vesting_upsert_manager.ts b/scripts/local_deploy_vesting_upsert_manager.ts deleted file mode 100644 index 2dc41fd..0000000 --- a/scripts/local_deploy_vesting_upsert_manager.ts +++ /dev/null @@ -1,47 +0,0 @@ -// scripts/deploy.ts -import { ethers, network } from "hardhat"; - -async function main() { - const [deployer] = await ethers.getSigners(); - - console.log("Deploying contracts with:", deployer.address); - - const admin = deployer.address; - const vestingAddress = "0x5733105364c8136226e246455328884c23151C60"; - const initializerAddress = "0xbf4B1201e3F2E862B48D763f4c6EAA5Ef0738B15"; - const defaultAdmin = "0x8CF65060CdA270a3886452A1A1cb656BECEE5bA4"; - const account1 = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; - - const vestingContract = await ethers.getContractAt("Vesting", vestingAddress); - const vestingPlanContract = await ethers.getContractAt("SymmVestingPlanInitializer", initializerAddress); - - const VestingUpsertManager = await ethers.getContractFactory("VestingUpsertManager"); - const upsertManager = await VestingUpsertManager.deploy(admin, vestingAddress, initializerAddress); - - await upsertManager.waitForDeployment(); - - console.log("VestingUpsertManager deployed to:", await upsertManager.getAddress()); - - await network.provider.request({ - method: "hardhat_impersonateAccount", - params: [defaultAdmin], - }); - - await deployer.sendTransaction({ - to: defaultAdmin, - value: ethers.parseEther("1") - }) - - const signer = await ethers.getSigner(defaultAdmin); - const OPERATOR_ROLE = await upsertManager.OPERATOR_ROLE(); - const SETTER_ROLE = await vestingContract.SETTER_ROLE(); - - await vestingContract.connect(signer).grantRole(OPERATOR_ROLE, account1); - await vestingContract.connect(signer).grantRole(SETTER_ROLE, await upsertManager.getAddress()); - await vestingPlanContract.connect(signer).grantRole(OPERATOR_ROLE, account1); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/scripts/future_reset_vesting_plans.py b/scripts/py/future_reset_vesting_plans.py similarity index 100% rename from scripts/future_reset_vesting_plans.py rename to scripts/py/future_reset_vesting_plans.py diff --git a/scripts/reset_vesting_plans.py b/scripts/py/reset_vesting_plans.py similarity index 100% rename from scripts/reset_vesting_plans.py rename to scripts/py/reset_vesting_plans.py diff --git a/scripts/vesting_setup.py b/scripts/py/vesting_setup.py similarity index 100% rename from scripts/vesting_setup.py rename to scripts/py/vesting_setup.py diff --git a/scripts/vesting_upsert_manager.py b/scripts/py/vesting_upsert_manager.py similarity index 100% rename from scripts/vesting_upsert_manager.py rename to scripts/py/vesting_upsert_manager.py From 8d619e3198dca695779d88ae4fecfacd34803d59 Mon Sep 17 00:00:00 2001 From: Naveed Date: Wed, 23 Jul 2025 09:32:49 +0200 Subject: [PATCH 30/48] Apply minor enhancements --- contracts/builders-nft/SymmioBuildersNft.sol | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index 56326f1..be8dc4d 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -2,26 +2,22 @@ pragma solidity ^0.8.27; /** - * @title SymmioBuildersNftUpgradeable - * @notice Upgradeable version of the advanced ERC721 NFT contract for locking SYMM tokens on the Base chain to enable + * @title SymmioBuildersNft + * @notice An advanced ERC721 NFT contract for locking SYMM tokens on the Base chain to enable * fee reductions across multiple chains. Each NFT represents a locked amount of SYMM * tokens with customizable branding and comprehensive unlock management capabilities. * * @dev Core features include: * • SYMM token locking with minimum amount requirements and token burning - * • NFT minting with associated brand names and lock data storage + * • NFT minting with associated builder names and lock data storage * • NFT merging functionality to consolidate locked amounts * • Partial unlock processes via external unlock manager integration * • Cross-chain synchronization for lock data consistency * • Fee collector integration for automatic fee reduction calculations - * • Granular pause controls for transfers and contract operations - * • Role-based access control for administrative and sync functions - * • Comprehensive view functions for user and system queries - * • TransparentUpgradeableProxy pattern for secure upgrades * * Integration points include external unlock manager for time-locked releases, * fee collector contracts for cross-chain fee reduction tracking, and sync - * mechanisms for maintaining consistency across multiple blockchain networks. + * mechanisms for maintaining consistency across multiple networks. */ import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; @@ -71,7 +67,7 @@ interface ISymmFeeCollector { function onLockedAmountChanged(int256 amount) external; } -contract SymmioBuildersNftUpgradeable is +contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, From e7a2b1261acab5a5a219d1dfa5a99ba009f6a5d1 Mon Sep 17 00:00:00 2001 From: Naveed Date: Wed, 23 Jul 2025 10:33:07 +0200 Subject: [PATCH 31/48] Simplify nft logic and movie complexity to the manager contract --- contracts/builders-nft/SymmioBuildersNft.sol | 619 +++-------------- .../builders-nft/SymmioBuildersNftManager.sol | 654 ++++++++++++++++++ .../builders-nft/SymmioUnlockManager.sol | 582 ---------------- 3 files changed, 732 insertions(+), 1123 deletions(-) create mode 100644 contracts/builders-nft/SymmioBuildersNftManager.sol delete mode 100644 contracts/builders-nft/SymmioUnlockManager.sol diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index be8dc4d..1e1b0fd 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -3,83 +3,26 @@ pragma solidity ^0.8.27; /** * @title SymmioBuildersNft - * @notice An advanced ERC721 NFT contract for locking SYMM tokens on the Base chain to enable - * fee reductions across multiple chains. Each NFT represents a locked amount of SYMM - * tokens with customizable branding and comprehensive unlock management capabilities. + * @notice A simple ERC721 NFT contract for Symmio Builders with brand name customization. + * All complex logic is handled by the SymmioBuildersNftManager contract. * - * @dev Core features include: - * • SYMM token locking with minimum amount requirements and token burning - * • NFT minting with associated builder names and lock data storage - * • NFT merging functionality to consolidate locked amounts - * • Partial unlock processes via external unlock manager integration - * • Cross-chain synchronization for lock data consistency - * • Fee collector integration for automatic fee reduction calculations - * - * Integration points include external unlock manager for time-locked releases, - * fee collector contracts for cross-chain fee reduction tracking, and sync - * mechanisms for maintaining consistency across multiple networks. + * @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/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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 Interface for the unlock manager contract handling token unlock processes. - * @dev Deployed separately to manage unlock operations for locked SYMM tokens. - */ -interface ISymmUnlockManager { - /** - * @notice Initiate the unlock process for a specified NFT. - * @param tokenId Token ID of the NFT to unlock. - * @param owner Owner address of the NFT. - * @param amount Amount of tokens to unlock. - */ - function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external; - - /** - * @notice Check if an NFT is currently in the unlocking process. - * @param tokenId Token ID to check. - * @return Whether the NFT is being unlocked. - */ - function isUnlocking(uint256 tokenId) external view returns (bool); -} - -/** - * @notice Interface for the fee collector contract handling fee collection. - */ -interface ISymmFeeCollector { - /** - * @notice Called when the locked amount of an NFT changes. - * @param amount Change in locked amount (positive for increase, negative for decrease). - */ - function onLockedAmountChanged(int256 amount) external; -} - -contract SymmioBuildersNft is - Initializable, - ERC721EnumerableUpgradeable, - AccessControlEnumerableUpgradeable, - PausableUpgradeable, - ReentrancyGuardUpgradeable -{ - using SafeERC20 for IERC20; +contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, PausableUpgradeable { /* ─────────────────────────────── Roles ─────────────────────────────── */ - /// @notice Role for updating configuration parameters like minimum lock amount and unlock manager. - bytes32 public constant SETTER_ROLE = keccak256("SETTER_ROLE"); + /// @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"); @@ -87,37 +30,13 @@ contract SymmioBuildersNft is /// @notice Role for unpausing the contract operations. bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); - /// @notice Role for pausing/unpausing NFT transfers specifically. - bytes32 public constant TRANSFER_PAUSER_ROLE = keccak256("TRANSFER_PAUSER_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. - IERC20Burnable public SYMM; - - /// @notice The unlock manager contract for handling token unlock processes. - ISymmUnlockManager public unlockManager; - - /// @notice The minimum amount of SYMM tokens required to mint an NFT. - uint256 public minLockAmount; - /// @notice Counter for generating unique token IDs sequentially. uint256 private _tokenIdCounter; - /// @notice Flag indicating whether NFT transfers are paused independently of contract pause. - bool public transfersPaused; - - /// @notice Mapping of token ID to its comprehensive lock data. - mapping(uint256 => LockData) public lockData; - - /// @notice Mapping of user address to their owned token IDs for internal tracking. - mapping(address => uint256[]) private _userTokens; - - /// @notice Mapping of token ID to its related fee collector addresses for fee reduction tracking. - mapping(uint256 => address[]) public tokenRelatedFeeCollectors; + /// @notice Mapping of token ID to its brand name. + mapping(uint256 => string) public brandNames; /// @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; @@ -125,21 +44,12 @@ contract SymmioBuildersNft is /* ─────────────────────────────── Events ─────────────────────────────── */ /** - * @notice Emitted when SYMM tokens are locked and an NFT is minted. - * @param user Address of the user locking tokens. + * @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 brandName Brand name associated with the NFT. */ - event TokenLocked(address indexed user, uint256 indexed tokenId, uint256 amount, string brandName); - - /** - * @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); + event NFTMinted(address indexed to, uint256 indexed tokenId, string brandName); /** * @notice Emitted when an NFT's brand name is updated. @@ -148,84 +58,10 @@ contract SymmioBuildersNft is */ event BrandNameUpdated(uint256 indexed tokenId, string newBrandName); - /** - * @notice Emitted when an unlock process is initiated for an NFT. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - * @param amount Amount of tokens to unlock. - */ - event UnlockInitiated(uint256 indexed tokenId, address indexed owner, uint256 amount); - - /** - * @notice Emitted when the minimum lock amount is updated. - * @param newMinAmount New minimum lock amount. - */ - event MinLockAmountUpdated(uint256 newMinAmount); - - /** - * @notice Emitted when the unlock manager address is updated. - * @param newUnlockManager New unlock manager address. - */ - event UnlockManagerUpdated(address newUnlockManager); - - /** - * @notice Emitted when the transfer pause state is updated. - * @param paused New pause state (true for paused, false for unpaused). - */ - event TransfersPausedUpdated(bool paused); - - /** - * @notice Emitted when an NFT is minted for cross-chain synchronization. - * @param to Address receiving the NFT. - * @param tokenId ID of the minted NFT. - * @param amount Amount of SYMM tokens locked. - * @param brandName Brand name associated with the NFT. - */ - event SyncMint(address indexed to, uint256 indexed tokenId, uint256 amount, string brandName); - - /** - * @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); // locked amount below minimum required error NotTokenOwner(); // caller is not the owner of the NFT - error InsufficientLockedAmount(); // requested unlock amount exceeds available locked amount - error InvalidTokenId(); // invalid token ID provided error ZeroAddress(); // zero address provided for critical parameters - error ZeroAmount(); // zero amount provided for operations requiring non-zero value - error TransfersPaused(); // NFT transfers are paused - error UnlockManagerNotSet(); // unlock manager is not set - error TokenHasActiveUnlock(); // NFT has an active unlock process - error UnauthorizedAccess(address caller, address requiredCaller); // unauthorized caller attempted restricted action - error LengthMismatch(); // input arrays have mismatched lengths - - /* ─────────────────────────────── Structs ─────────────────────────────── */ - - /** - * @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 brandName Custom brand name associated with the NFT. - * @param unlockingAmount Amount of tokens currently being unlocked. - */ - struct LockData { - uint256 amount; - uint256 lockTimestamp; - string brandName; - uint256 unlockingAmount; - } /* ─────────────────────────── Initialization ─────────────────────────── */ @@ -235,127 +71,78 @@ contract SymmioBuildersNft is } /** - * @notice Initialize the upgradeable SymmioBuildersNft contract with core parameters. - * @param _symm Address of the SYMM token contract. - * @param _admin Address to receive admin and all role assignments. - * @param _minLockAmount Minimum amount of SYMM tokens required to mint an NFT. + * @notice Initialize the upgradeable SymmioBuildersNft contract. + * @param _admin Address to receive admin and all role assignments. + * @param _manager Address of the manager contract to receive minter/burner roles. * - * @dev Sets up the ERC721 contract, assigns comprehensive roles, and validates inputs. - * This function replaces the constructor for upgradeable contracts. + * @dev Sets up the ERC721 contract and assigns roles. */ - function initialize(address _symm, address _admin, uint256 _minLockAmount) public initializer { - if (_symm == address(0) || _admin == address(0)) revert ZeroAddress(); - if (_minLockAmount == 0) revert ZeroAmount(); + function initialize(address _admin, address _manager) public initializer { + if (_admin == address(0) || _manager == address(0)) revert ZeroAddress(); // Initialize parent contracts __ERC721_init("Symmio Builders NFT", "BUILDERS"); __ERC721Enumerable_init(); __AccessControlEnumerable_init(); __Pausable_init(); - __ReentrancyGuard_init(); - // Set contract-specific state - SYMM = IERC20Burnable(_symm); - minLockAmount = _minLockAmount; - - // Grant all roles to the admin for initial setup + // Grant roles to admin _grantRole(DEFAULT_ADMIN_ROLE, _admin); - _grantRole(SETTER_ROLE, _admin); _grantRole(PAUSER_ROLE, _admin); _grantRole(UNPAUSER_ROLE, _admin); - _grantRole(TRANSFER_PAUSER_ROLE, _admin); - _grantRole(SYNC_ROLE, _admin); + + // Grant minter and burner roles to manager + _grantRole(MINTER_ROLE, _manager); + _grantRole(BURNER_ROLE, _manager); } - /* ────────────────────── Core NFT & Locking Functions ────────────────────── */ + /* ────────────────────────── Core 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. + * @notice Mint a new NFT with a brand name. + * @param to Address to mint the NFT to. + * @param brandName 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. + * @dev Only callable by addresses with MINTER_ROLE (typically the manager contract). */ - function mintAndLock(uint256 amount, string memory brandName) external nonReentrant whenNotPaused returns (uint256 tokenId) { - if (amount < minLockAmount) revert AmountBelowMinimum(amount, minLockAmount); - - // Burn the SYMM tokens by transferring to zero address - SYMM.burnFrom(msg.sender, amount); - - // Mint new NFT with the next available token ID + function mint(address to, string memory brandName) external onlyRole(MINTER_ROLE) whenNotPaused returns (uint256 tokenId) { tokenId = _tokenIdCounter++; - _safeMint(msg.sender, tokenId); - - // Store comprehensive lock data for the NFT - lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, brandName: brandName, unlockingAmount: 0 }); - - // Notify all related fee collectors of the locked amount increase - for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) - ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); + _safeMint(to, tokenId); + brandNames[tokenId] = brandName; - emit TokenLocked(msg.sender, tokenId, amount, brandName); + emit NFTMinted(to, tokenId, 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. + * @notice Mint a specific NFT ID with a brand name (used for cross-chain sync). + * @param to Address to mint the NFT to. + * @param tokenId Specific token ID to mint. + * @param brandName Brand name for the NFT. * - * @dev Burns the SYMM tokens and increases the locked amount for the NFT. + * @dev Only callable by addresses with MINTER_ROLE. Updates counter to avoid conflicts. */ - function lock(uint256 tokenId, uint256 amount) external nonReentrant whenNotPaused { - if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); - - // Burn the SYMM tokens by transferring to zero address - SYMM.burnFrom(msg.sender, amount); - - // Increase the locked amount for the NFT - lockData[tokenId].amount += amount; + function mintWithId(address to, uint256 tokenId, string memory brandName) external onlyRole(MINTER_ROLE) whenNotPaused { + // Update token ID counter to avoid conflicts + if (tokenId >= _tokenIdCounter) { + _tokenIdCounter = tokenId + 1; + } - // Notify all related fee collectors of the locked amount increase - for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) - ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); + _safeMint(to, tokenId); + brandNames[tokenId] = brandName; - emit TokenLocked(msg.sender, tokenId, amount, lockData[tokenId].brandName); + emit NFTMinted(to, tokenId, brandName); } - /* ────────────────────────── 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). + * @notice Burn an NFT. + * @param tokenId ID of the NFT to burn. * - * @dev Combines locked amounts, burns source NFT, updates target NFT, and notifies fee collectors. - * Reverts if either NFT has an active unlock process. + * @dev Only callable by addresses with BURNER_ROLE (typically the manager contract). */ - function merge(uint256 targetTokenId, uint256 sourceTokenId) external nonReentrant whenNotPaused { - if (ownerOf(targetTokenId) != msg.sender) revert NotTokenOwner(); - if (ownerOf(sourceTokenId) != msg.sender) revert NotTokenOwner(); - - LockData storage targetData = lockData[targetTokenId]; - LockData storage sourceData = lockData[sourceTokenId]; - - if (targetData.unlockingAmount > 0 || sourceData.unlockingAmount > 0) revert TokenHasActiveUnlock(); - - // Merge locked amounts into the target NFT - uint256 newAmount = targetData.amount + sourceData.amount; - targetData.amount = newAmount; - - // Burn the source NFT and clear its lock data - _burn(sourceTokenId); - delete lockData[sourceTokenId]; - - // Notify fee collectors for the target NFT (increase) - for (uint256 i = 0; i < tokenRelatedFeeCollectors[targetTokenId].length; i++) - ISymmFeeCollector(tokenRelatedFeeCollectors[targetTokenId][i]).onLockedAmountChanged(int256(sourceData.amount)); - - // Notify fee collectors for the source NFT (decrease) - for (uint256 i = 0; i < tokenRelatedFeeCollectors[sourceTokenId].length; i++) - ISymmFeeCollector(tokenRelatedFeeCollectors[sourceTokenId][i]).onLockedAmountChanged(-int256(sourceData.amount)); - - emit TokensMerged(targetTokenId, sourceTokenId, newAmount); + function burn(uint256 tokenId) external onlyRole(BURNER_ROLE) { + _burn(tokenId); + delete brandNames[tokenId]; } /** @@ -368,137 +155,31 @@ contract SymmioBuildersNft is function updateBrandName(uint256 tokenId, string memory newBrandName) external { if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); - lockData[tokenId].brandName = newBrandName; + brandNames[tokenId] = newBrandName; emit BrandNameUpdated(tokenId, newBrandName); } - /* ──────────────────────── 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. - * - * @dev Updates the unlocking amount, calls the unlock manager, and notifies fee collectors. - */ - function initiateUnlock(uint256 tokenId, uint256 amount) external nonReentrant { - if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); - if (address(unlockManager) == address(0)) revert UnlockManagerNotSet(); - - LockData storage data = lockData[tokenId]; - uint256 availableAmount = data.amount - data.unlockingAmount; - - if (amount > availableAmount) revert InsufficientLockedAmount(); - if (amount == 0) revert ZeroAmount(); - - // Update the unlocking amount - data.unlockingAmount += amount; - - // Delegate to the unlock manager - unlockManager.initiateUnlock(tokenId, msg.sender, amount); - - // Notify fee collectors of the effective decrease in locked amount - for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) - ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(-int256(amount)); - - emit UnlockInitiated(tokenId, msg.sender, amount); - } - - /** - * @notice Complete the unlock process for an NFT, transferring tokens to the unlock manager. - * @param tokenId ID of the NFT to unlock. - * @param amount Amount of tokens being unlocked. - * - * @dev Only callable by the unlock manager. Burns the NFT if no tokens remain. - */ - function completeUnlock(uint256 tokenId, uint256 amount) external { - if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); - - LockData storage data = lockData[tokenId]; - data.unlockingAmount -= amount; - data.amount -= amount; - - // Burn the NFT if no locked tokens remain - if (data.amount == 0) { - _burn(tokenId); - delete lockData[tokenId]; - } - } - - /** - * @notice Cancel an unlock process for an NFT, restoring the unlocking amount. - * @param tokenId ID of the NFT to cancel the unlock for. - * @param amount Amount to cancel from the unlocking process. - * - * @dev Only callable by the unlock manager. Restores effective locked amount. - */ - function cancelUnlock(uint256 tokenId, uint256 amount) external { - if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); - - lockData[tokenId].unlockingAmount -= amount; - - // Notify fee collectors of the effective increase in locked amount - for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) - ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); - } - - /* ───────────────────── Cross-Chain Sync Functions ───────────────────── */ - - /** - * @notice Mint an NFT without token transfer for cross-chain synchronization. - * @param to Address to mint the NFT to. - * @param tokenId Specific token ID to mint. - * @param amount Amount of SYMM tokens locked. - * @param brandName Brand name for the NFT. - * - * @dev Only callable by accounts with SYNC_ROLE. Used for cross-chain lock data sync. - */ - function syncMint(address to, uint256 tokenId, uint256 amount, string memory brandName) external onlyRole(SYNC_ROLE) whenNotPaused { - // Update token ID counter to avoid conflicts with future mints - if (tokenId >= _tokenIdCounter) { - _tokenIdCounter = tokenId + 1; - } - - // Mint NFT to the specified address - _safeMint(to, tokenId); - - // Store lock data for the NFT - lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, brandName: brandName, unlockingAmount: 0 }); - - // Notify all related fee collectors of the locked amount - for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) - ISymmFeeCollector(tokenRelatedFeeCollectors[tokenId][i]).onLockedAmountChanged(int256(amount)); - - emit SyncMint(to, tokenId, amount, brandName); - } + /* ────────────────────────── View 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. - * - * @dev Only callable by accounts with SYNC_ROLE. Arrays must have matching lengths. + * @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 batchUpdateLockData(uint256[] calldata tokenIds, LockData[] calldata lockDatas) external onlyRole(SYNC_ROLE) { - if (tokenIds.length != lockDatas.length) revert LengthMismatch(); - - // Update lock data for each token ID - for (uint256 i = 0; i < tokenIds.length; i++) { - uint256 oldAmount = lockData[tokenIds[i]].amount; - uint256 newAmount = lockDatas[i].amount; - lockData[tokenIds[i]] = lockDatas[i]; - - // Notify all related fee collectors of the locked amount - for (uint256 j = 0; j < tokenRelatedFeeCollectors[tokenIds[i]].length; j++) - ISymmFeeCollector(tokenRelatedFeeCollectors[tokenIds[i]][j]).onLockedAmountChanged(int256(newAmount) - int256(oldAmount)); + 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; } /* ───────────────────────── Pause Controls ───────────────────────── */ /** - * @notice Pause the contract, disabling state-changing functions. + * @notice Pause the contract, disabling minting and transfers. * @dev Only callable by accounts with PAUSER_ROLE. */ function pause() external onlyRole(PAUSER_ROLE) { @@ -506,169 +187,19 @@ contract SymmioBuildersNft is } /** - * @notice Unpause the contract, enabling state-changing functions. + * @notice Unpause the contract, enabling minting and transfers. * @dev Only callable by accounts with UNPAUSER_ROLE. */ function unpause() external onlyRole(UNPAUSER_ROLE) { _unpause(); } - /** - * @notice Set the pause state for NFT transfers independently of contract pause. - * @param _paused True to pause transfers, false to unpause. - * - * @dev Only callable by accounts with TRANSFER_PAUSER_ROLE. - */ - function setTransfersPaused(bool _paused) external onlyRole(TRANSFER_PAUSER_ROLE) { - transfersPaused = _paused; - emit TransfersPausedUpdated(_paused); - } - - /* ────────────────────────── Admin Functions ────────────────────────── */ - - /** - * @notice Set the minimum lock amount for minting NFTs. - * @param _minLockAmount New minimum lock amount. - * - * @dev Only callable by accounts with SETTER_ROLE. - */ - function setMinLockAmount(uint256 _minLockAmount) external onlyRole(SETTER_ROLE) { - if (_minLockAmount == 0) revert ZeroAmount(); - minLockAmount = _minLockAmount; - emit MinLockAmountUpdated(_minLockAmount); - } - - /** - * @notice Set the address of the unlock manager contract. - * @param _unlockManager New unlock manager address. - * - * @dev Only callable by accounts with SETTER_ROLE. - */ - function setUnlockManager(address _unlockManager) external onlyRole(SETTER_ROLE) { - if (_unlockManager == address(0)) revert ZeroAddress(); - unlockManager = ISymmUnlockManager(_unlockManager); - emit UnlockManagerUpdated(_unlockManager); - } - - /** - * @notice Add fee collectors to an NFT for fee reduction tracking. - * @param tokenId ID of the NFT to add fee collectors to. - * @param feeCollectors Array of fee collector addresses to add. - * - * @dev Only callable by accounts with SETTER_ROLE. - */ - 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. - * - * @dev Only callable by accounts with SETTER_ROLE. Uses swap-and-pop for gas efficiency. - */ - function removeFeeCollector(uint256 tokenId, address feeCollector) external onlyRole(SETTER_ROLE) { - for (uint256 i = 0; i < tokenRelatedFeeCollectors[tokenId].length; i++) { - if (tokenRelatedFeeCollectors[tokenId][i] == feeCollector) { - tokenRelatedFeeCollectors[tokenId][i] = tokenRelatedFeeCollectors[tokenId][tokenRelatedFeeCollectors[tokenId].length - 1]; - tokenRelatedFeeCollectors[tokenId].pop(); - break; - } - } - emit FeeCollectorRemoved(tokenId, feeCollector); - } - - /* ────────────────────────── Version & Info ────────────────────────── */ + /* ───────────────────────── Internal Overrides ───────────────────────── */ /** - * @notice Get the current contract version. - * @return The version string of the current contract. - * - * @dev This can be overridden in future upgrades to track versions. + * @dev Override _update to add pause check for transfers. */ - function version() external pure returns (string memory) { - return "1.0.0"; - } - - /* ────────────────────────── View Functions ────────────────────────── */ - - /** - * @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 lock data for multiple NFTs in a single call. - * @param tokenIds Array of token IDs to query. - * @return Array of LockData structs. - */ - function getLockDataBatch(uint256[] calldata tokenIds) external view returns (LockData[] memory) { - LockData[] memory result = new LockData[](tokenIds.length); - for (uint256 i = 0; i < tokenIds.length; i++) { - result[i] = lockData[tokenIds[i]]; - } - return result; - } - - /** - * @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); - } - } - - /* ───────────────────────── Internal Helpers ───────────────────────── */ - - /** - * @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(); - } - + function _update(address to, uint256 tokenId, address auth) internal virtual override whenNotPaused returns (address) { return super._update(to, tokenId, auth); } @@ -678,12 +209,18 @@ contract SymmioBuildersNft is * @notice Check if the contract supports a given interface. * @param interfaceId Interface ID to check. * @return Whether the interface is supported. - * - * @dev Supports ERC721Enumerable and AccessControlEnumerable interfaces. */ function supportsInterface( bytes4 interfaceId ) public view override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable) 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..f907357 --- /dev/null +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -0,0 +1,654 @@ +// 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, merging, fee collection, and cross-chain sync. + * + * @dev Core features include: + * • SYMM token locking with burning and without burning (for MINTER_ROLE) + * • Lock data management for all NFTs + * • NFT merging functionality + * • Integration with unlock manager for time-locked releases + * • Fee collector management and notifications + * • Cross-chain synchronization capabilities + * • Transfer restrictions based on unlock status + * + * This contract acts as the central logic hub while the NFT contract remains simple. + */ + +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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 Interface for the SymmioBuildersNft contract. + */ +interface ISymmioBuildersNft { + function mint(address to, string memory brandName) external returns (uint256 tokenId); + + function mintWithId(address to, uint256 tokenId, string memory brandName) external; + + function burn(uint256 tokenId) external; + + function ownerOf(uint256 tokenId) external view returns (address); + + function brandNames(uint256 tokenId) external view returns (string memory); +} + +/** + * @notice Interface for the unlock manager contract handling token unlock processes. + */ +interface ISymmUnlockManager { + function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external; + + function isUnlocking(uint256 tokenId) external view returns (bool); +} + +/** + * @notice Interface for the fee collector contract handling fee collection. + */ +interface ISymmFeeCollector { + function onLockedAmountChanged(int256 amount) external; +} + +contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20; + + /* ─────────────────────────────── Roles ─────────────────────────────── */ + + /// @notice Role for minting NFTs without burning SYMM tokens. + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + /// @notice Role for updating configuration parameters. + bytes32 public constant SETTER_ROLE = keccak256("SETTER_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"); + + /// @notice Role for pausing/unpausing NFT transfers specifically. + bytes32 public constant TRANSFER_PAUSER_ROLE = keccak256("TRANSFER_PAUSER_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. + IERC20Burnable public SYMM; + + /// @notice The SymmioBuildersNft contract. + ISymmioBuildersNft public nftContract; + + /// @notice The unlock manager contract for handling token unlock processes. + ISymmUnlockManager public unlockManager; + + /// @notice The minimum amount of SYMM tokens required to mint an NFT. + uint256 public minLockAmount; + + /// @notice Flag indicating whether NFT transfers are paused. + bool public transfersPaused; + + /// @notice Mapping of token ID to its comprehensive lock data. + mapping(uint256 => LockData) public lockData; + + /// @notice Mapping of token ID to its related fee collector addresses. + mapping(uint256 => address[]) public tokenRelatedFeeCollectors; + + /// @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 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. + * @param brandName Brand name associated with the NFT. + */ + event TokenLocked(address indexed user, uint256 indexed tokenId, uint256 amount, string brandName); + + /** + * @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 brandName Brand name associated with the NFT. + */ + event NFTMintedWithoutBurn(address indexed minter, address indexed to, uint256 indexed tokenId, uint256 amount, string brandName); + + /** + * @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 tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens to unlock. + */ + event UnlockInitiated(uint256 indexed tokenId, address indexed owner, uint256 amount); + + /** + * @notice Emitted when the minimum lock amount is updated. + * @param newMinAmount New minimum lock amount. + */ + event MinLockAmountUpdated(uint256 newMinAmount); + + /** + * @notice Emitted when the unlock manager address is updated. + * @param newUnlockManager New unlock manager address. + */ + event UnlockManagerUpdated(address newUnlockManager); + + /** + * @notice Emitted when the transfer pause state is updated. + * @param paused New pause state (true for paused, false for unpaused). + */ + event TransfersPausedUpdated(bool paused); + + /** + * @notice Emitted when an NFT is minted for cross-chain synchronization. + * @param to Address receiving the NFT. + * @param tokenId ID of the minted NFT. + * @param amount Amount of SYMM tokens locked. + * @param brandName Brand name associated with the NFT. + */ + event SyncMint(address indexed to, uint256 indexed tokenId, uint256 amount, string brandName); + + /** + * @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 ZeroAddress(); + error ZeroAmount(); + error TransfersPaused(); + error UnlockManagerNotSet(); + error TokenHasActiveUnlock(); + error UnauthorizedAccess(address caller, address requiredCaller); + error LengthMismatch(); + + /* ─────────────────────────────── Structs ─────────────────────────────── */ + + /** + * @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; + } + + /* ─────────────────────────────── Modifiers ─────────────────────────────── */ + + /** + * @notice Ensure transfers are not paused and token has no active unlock. + * @param tokenId ID of the token to check. + */ + modifier transfersAllowed(uint256 tokenId) { + if (transfersPaused) revert TransfersPaused(); + if (lockData[tokenId].unlockingAmount > 0) revert TokenHasActiveUnlock(); + _; + } + + /* ─────────────────────────── Initialization ─────────────────────────── */ + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the SymmioBuildersNftManager contract. + * @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. + */ + function initialize(address _symm, address _nftContract, address _admin, uint256 _minLockAmount) public initializer { + if (_symm == address(0) || _nftContract == address(0) || _admin == address(0)) revert ZeroAddress(); + if (_minLockAmount == 0) revert ZeroAmount(); + + // Initialize parent contracts + __AccessControlEnumerable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + // Set contract-specific state + SYMM = IERC20Burnable(_symm); + nftContract = ISymmioBuildersNft(_nftContract); + minLockAmount = _minLockAmount; + + // Grant all roles to the admin for initial setup + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(MINTER_ROLE, _admin); + _grantRole(SETTER_ROLE, _admin); + _grantRole(PAUSER_ROLE, _admin); + _grantRole(UNPAUSER_ROLE, _admin); + _grantRole(TRANSFER_PAUSER_ROLE, _admin); + _grantRole(SYNC_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, brandName); + + // Store lock data + lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0 }); + + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); + + emit TokenLocked(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); + + // Mint new NFT + tokenId = nftContract.mint(to, brandName); + + // Store lock data (same as regular mint) + lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0 }); + + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); + + 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 (nftContract.ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); + + // Burn the SYMM tokens + SYMM.burnFrom(msg.sender, amount); + + // Increase the locked amount + lockData[tokenId].amount += amount; + + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); + + emit TokenLocked(msg.sender, tokenId, amount, nftContract.brandNames(tokenId)); + } + + /* ────────────────────────── 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) revert NotTokenOwner(); + if (nftContract.ownerOf(sourceTokenId) != msg.sender) revert NotTokenOwner(); + + LockData storage targetData = lockData[targetTokenId]; + LockData storage sourceData = lockData[sourceTokenId]; + + if (targetData.unlockingAmount > 0 || sourceData.unlockingAmount > 0) revert TokenHasActiveUnlock(); + + // Merge locked amounts + uint256 newAmount = targetData.amount + sourceData.amount; + targetData.amount = newAmount; + + // 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); + delete lockData[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 { + if (nftContract.ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); + if (address(unlockManager) == address(0)) revert UnlockManagerNotSet(); + + LockData storage data = lockData[tokenId]; + uint256 availableAmount = data.amount - data.unlockingAmount; + + if (amount > availableAmount) revert InsufficientLockedAmount(); + if (amount == 0) revert ZeroAmount(); + + // Update the unlocking amount + data.unlockingAmount += amount; + + // Delegate to the unlock manager + unlockManager.initiateUnlock(tokenId, msg.sender, amount); + + // Notify fee collectors + _notifyFeeCollectors(tokenId, -int256(amount)); + + emit UnlockInitiated(tokenId, msg.sender, amount); + } + + /** + * @notice Complete the unlock process for an NFT. + * @param tokenId ID of the NFT to unlock. + * @param amount Amount of tokens being unlocked. + * + * @dev Only callable by the unlock manager. Burns NFT if no tokens remain. + */ + function completeUnlock(uint256 tokenId, uint256 amount) external { + if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); + + LockData storage data = lockData[tokenId]; + data.unlockingAmount -= amount; + data.amount -= amount; + + // Burn the NFT if no locked tokens remain + if (data.amount == 0) { + nftContract.burn(tokenId); + delete lockData[tokenId]; + } + } + + /** + * @notice Cancel an unlock process for an NFT. + * @param tokenId ID of the NFT to cancel the unlock for. + * @param amount Amount to cancel from the unlocking process. + * + * @dev Only callable by the unlock manager. + */ + function cancelUnlock(uint256 tokenId, uint256 amount) external { + if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); + + lockData[tokenId].unlockingAmount -= amount; + + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); + } + + /* ───────────────────── Cross-Chain Sync Functions ───────────────────── */ + + /** + * @notice Mint an NFT without token transfer for cross-chain synchronization. + * @param to Address to mint the NFT to. + * @param tokenId Specific token ID to mint. + * @param amount Amount of SYMM tokens locked. + * @param brandName Brand name for the NFT. + */ + function syncMint(address to, uint256 tokenId, uint256 amount, string memory brandName) external onlyRole(SYNC_ROLE) whenNotPaused { + // Mint NFT with specific ID + nftContract.mintWithId(to, tokenId, brandName); + + // Store lock data + lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0 }); + + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); + + emit SyncMint(to, tokenId, amount, brandName); + } + + /** + * @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, LockData[] calldata lockDatas) external onlyRole(SYNC_ROLE) { + if (tokenIds.length != lockDatas.length) revert LengthMismatch(); + + for (uint256 i = 0; i < tokenIds.length; i++) { + uint256 oldAmount = lockData[tokenIds[i]].amount; + uint256 newAmount = lockDatas[i].amount; + lockData[tokenIds[i]] = lockDatas[i]; + + // Notify fee collectors of the change + _notifyFeeCollectors(tokenIds[i], int256(newAmount) - int256(oldAmount)); + } + } + + /* ───────────────────────── Transfer Controls ───────────────────────── */ + + /** + * @notice Check if an NFT transfer is allowed. + * @param tokenId ID of the NFT to check. + * @return Whether the transfer is allowed. + */ + function isTransferAllowed(uint256 tokenId) external view returns (bool) { + return !transfersPaused && lockData[tokenId].unlockingAmount == 0; + } + + /** + * @notice Hook called before NFT transfers to check restrictions. + * @param from Address transferring from. + * @param to Address transferring to. + * @param tokenId ID of the NFT being transferred. + * + * @dev Should be called by the NFT contract before transfers. + */ + function beforeTokenTransfer(address from, address to, uint256 tokenId) external view { + // Skip checks for minting (from == address(0)) and burning (to == address(0)) + if (from != address(0) && to != address(0)) { + if (transfersPaused) revert TransfersPaused(); + if (lockData[tokenId].unlockingAmount > 0) revert TokenHasActiveUnlock(); + } + } + + /* ───────────────────────── Pause Controls ───────────────────────── */ + + /** + * @notice Pause the contract, disabling state-changing functions. + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /** + * @notice Unpause the contract, enabling state-changing functions. + */ + function unpause() external onlyRole(UNPAUSER_ROLE) { + _unpause(); + } + + /** + * @notice Set the pause state for NFT transfers. + * @param _paused True to pause transfers, false to unpause. + */ + function setTransfersPaused(bool _paused) external onlyRole(TRANSFER_PAUSER_ROLE) { + transfersPaused = _paused; + emit TransfersPausedUpdated(_paused); + } + + /* ────────────────────────── 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 Set the address of the unlock manager contract. + * @param _unlockManager New unlock manager address. + */ + function setUnlockManager(address _unlockManager) external onlyRole(SETTER_ROLE) { + if (_unlockManager == address(0)) revert ZeroAddress(); + unlockManager = ISymmUnlockManager(_unlockManager); + emit UnlockManagerUpdated(_unlockManager); + } + + /** + * @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 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 lock data for multiple NFTs in a single call. + * @param tokenIds Array of token IDs to query. + * @return Array of LockData structs. + */ + function getLockDataBatch(uint256[] calldata tokenIds) external view returns (LockData[] memory) { + LockData[] memory result = new LockData[](tokenIds.length); + for (uint256 i = 0; i < tokenIds.length; i++) { + result[i] = lockData[tokenIds[i]]; + } + return result; + } + + /** + * @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) { + // This would need to iterate through user's NFTs from the NFT contract + // Implementation depends on how the NFT contract exposes user's tokens + // For now, returning 0 as placeholder + return 0; + } + + /** + * @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]; + } + + /* ───────────────────────── 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++) { + ISymmFeeCollector(collectors[i]).onLockedAmountChanged(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/SymmioUnlockManager.sol b/contracts/builders-nft/SymmioUnlockManager.sol deleted file mode 100644 index 85bf4af..0000000 --- a/contracts/builders-nft/SymmioUnlockManager.sol +++ /dev/null @@ -1,582 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -/** - * @title SymmUnlockManager - * @notice Manages the unlock process for SYMM tokens locked in SymmioBuildersNFT contracts with - * cliff periods and vesting integration. Provides a complete workflow for token unlocking - * including request initiation, cliff enforcement, cancellation capabilities, and vesting setup. - * - * @dev Core features include: - * • Unlock request management with unique ID tracking - * • Configurable cliff period enforcement before token release - * • Integration with external vesting contracts for gradual token release - * • Cancellation functionality for unlock requests during cliff period - * • Comprehensive tracking of unlock status and timing - * • Role-based access control for administrative functions - * • Emergency pause functionality for security incidents - * • Token rescue capabilities for administrative recovery - * • Detailed view functions for unlock request analysis - * - * The contract coordinates between SymmioBuildersNFT for lock management and external - * vesting contracts for token distribution, ensuring secure and controlled token unlocking - * with configurable time-based restrictions and user flexibility. - * - * @dev This contract is designed to be used with OpenZeppelin's TransparentUpgradeableProxy. - */ - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; - -/* ────────────────────────── External Interfaces ────────────────────────── */ - -/** - * @notice Interface for the SymmioBuildersNFT contract to query ownership and lock data. - */ -interface ISymmioBuildersNft { - /** - * @notice Get the owner of a specific NFT. - * @param tokenId ID of the NFT to query. - * @return Address of the NFT owner. - */ - function ownerOf(uint256 tokenId) external view returns (address); - - /** - * @notice Get the lock data for a specific NFT. - * @param tokenId ID of the NFT to query. - * @return amount Amount of tokens locked. - * @return lockTimestamp Timestamp when tokens were locked. - * @return brandName Brand name associated with the NFT. - * @return unlockingAmount Amount currently being unlocked. - */ - function lockData( - uint256 tokenId - ) external view returns (uint256 amount, uint256 lockTimestamp, string memory brandName, uint256 unlockingAmount); - - /** - * @notice Complete the unlock process for an NFT. - * @param tokenId ID of the NFT to unlock. - * @param amount Amount of tokens to unlock. - */ - function completeUnlock(uint256 tokenId, uint256 amount) external; - - /** - * @notice Cancel an unlock process for an NFT. - * @param tokenId ID of the NFT to cancel unlock for. - * @param amount Amount to cancel from the unlock process. - */ - function cancelUnlock(uint256 tokenId, uint256 amount) external; -} - -/** - * @notice Interface for the Vesting contract to set up vesting plans. - */ -interface IVesting { - /** - * @notice 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 user. - */ - function setupVestingPlans(address token, uint256 startTime, uint256 endTime, address[] memory users, uint256[] memory amounts) external; -} - -contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { - using SafeERC20 for IERC20; - - /* ─────────────────────────────── Roles ─────────────────────────────── */ - - /// @notice Role for updating configuration parameters like cliff and vesting durations. - bytes32 public constant SETTER_ROLE = keccak256("SETTER_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 The SymmioBuildersNFT contract. - ISymmioBuildersNft public symmBuildersNft; - - /// @notice The Vesting contract for managing token vesting plans. - IVesting public vestingContract; - - /// @notice The SYMM token contract. - IERC20 public SYMM; - - /// @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 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; - - /// @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; - - /* ─────────────────────────────── 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. - */ - struct UnlockRequest { - uint256 amount; - uint256 unlockInitiatedTime; - address owner; - uint256 tokenId; - bool cliffPassed; - bool vestingStarted; - } - - /* ─────────────────────────────── Events ─────────────────────────────── */ - - /** - * @notice Emitted when an unlock request is initiated. - * @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 request 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 the cliff period for an unlock request is completed. - * @param unlockId ID of the unlock request. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - */ - event CliffCompleted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner); - - /** - * @notice Emitted when vesting starts for an unlock request. - * @param unlockId ID of the unlock request. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - * @param amount Amount of tokens entering vesting. - */ - event VestingStarted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount); - - /** - * @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 the vesting contract address is updated. - * @param newVestingContract New vesting contract address. - */ - event VestingContractUpdated(address newVestingContract); - - /* ─────────────────────────────── Errors ─────────────────────────────── */ - - error NotNFTOwner(); // caller is not the owner of the NFT - error UnlockNotFound(); // unlock request ID is invalid or not found - error CliffNotPassed(); // cliff period has not yet passed - error VestingAlreadyStarted(); // vesting has already started for this unlock request - error InvalidDuration(); // invalid duration (zero) provided for cliff or vesting - error ZeroAddress(); // zero address provided for critical parameters - error ZeroAmount(); // zero amount provided for operations requiring non-zero value - error UnauthorizedAccess(address caller, address requiredCaller); // unauthorized caller attempted restricted action - - /* ─────────────────────────── Initialization ─────────────────────────── */ - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /** - * @notice Initialize the SymmUnlockManager with core contracts and configuration. - * @param _symmBuildersNft Address of the SymmioBuildersNFT contract. - * @param _symm Address of the SYMM token contract. - * @param _vestingContract Address of the Vesting contract. - * @param _admin Address to receive admin and all role assignments. - * @param _cliffDuration Duration of the cliff period in seconds. - * @param _vestingDuration Duration of the vesting period in seconds. - * - * @dev Sets up access control and validates all inputs. Reverts on zero addresses or invalid durations. - * This replaces the constructor for upgradeable contracts. - */ - function initialize( - address _symmBuildersNft, - address _symm, - address _vestingContract, - address _admin, - uint256 _cliffDuration, - uint256 _vestingDuration - ) public initializer { - if (_symmBuildersNft == address(0) || _symm == address(0) || _vestingContract == address(0) || _admin == address(0)) { - revert ZeroAddress(); - } - if (_cliffDuration == 0 || _vestingDuration == 0) { - revert InvalidDuration(); - } - - // Initialize parent contracts - __AccessControlEnumerable_init(); - __Pausable_init(); - __ReentrancyGuard_init(); - - // Set contract addresses and parameters - symmBuildersNft = ISymmioBuildersNft(_symmBuildersNft); - SYMM = IERC20(_symm); - vestingContract = IVesting(_vestingContract); - cliffDuration = _cliffDuration; - vestingDuration = _vestingDuration; - - // Initialize counter - _unlockIdCounter = 0; - - // Grant roles to admin - _grantRole(DEFAULT_ADMIN_ROLE, _admin); - _grantRole(SETTER_ROLE, _admin); - _grantRole(PAUSER_ROLE, _admin); - _grantRole(UNPAUSER_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(); - } - - /* ──────────────────── Unlock Management ──────────────────── */ - - /** - * @notice Initiate an unlock request for an NFT (called by SymmioBuildersNFT contract). - * @param tokenId ID of the NFT to unlock. - * @param owner Owner of the NFT. - * @param amount Amount of tokens to unlock. - * - * @dev Creates a new unlock request with cliff period enforcement. - * Only callable by the SymmioBuildersNFT contract. - */ - function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external whenNotPaused { - if (msg.sender != address(symmBuildersNft)) { - revert UnauthorizedAccess(msg.sender, address(symmBuildersNft)); - } - if (amount == 0) { - revert ZeroAmount(); - } - - uint256 unlockId = _unlockIdCounter++; - unlockRequests[unlockId] = UnlockRequest({ - amount: amount, - unlockInitiatedTime: block.timestamp, - owner: owner, - tokenId: tokenId, - cliffPassed: false, - vestingStarted: false - }); - - tokenUnlockIds[tokenId].push(unlockId); - - emit UnlockInitiated(unlockId, tokenId, owner, 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 notifies the 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 (symmBuildersNft.ownerOf(request.tokenId) != msg.sender) { - revert NotNFTOwner(); - } - if (request.cliffPassed) { - revert CliffNotPassed(); - } - - 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; - } - } - - // Notify NFT contract to cancel the unlock - symmBuildersNft.cancelUnlock(tokenId, 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 Transfers tokens to vesting contract and sets up 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 (symmBuildersNft.ownerOf(request.tokenId) != msg.sender) { - revert NotNFTOwner(); - } - if (request.vestingStarted) { - revert VestingAlreadyStarted(); - } - if (block.timestamp < request.unlockInitiatedTime + cliffDuration) { - revert CliffNotPassed(); - } - - // Mark cliff as passed and vesting as started - request.cliffPassed = true; - request.vestingStarted = true; - - // Complete unlock on NFT contract - symmBuildersNft.completeUnlock(request.tokenId, request.amount); - - // Set up vesting plan for the owner - address[] memory users = new address[](1); - users[0] = request.owner; - uint256[] memory amounts = new uint256[](1); - amounts[0] = request.amount; - - // Approve vesting contract to transfer tokens - SYMM.approve(address(vestingContract), request.amount); - - // Set up vesting plan starting from now - vestingContract.setupVestingPlans(address(SYMM), block.timestamp, block.timestamp + vestingDuration, users, amounts); - - emit CliffCompleted(unlockId, request.tokenId, request.owner); - emit VestingStarted(unlockId, request.tokenId, request.owner, request.amount); - } - - /* ────────────────────────── Admin Functions ────────────────────────── */ - - /** - * @notice Update the cliff duration for new unlock requests. - * @param _cliffDuration New cliff duration in seconds. - * - * @dev Only callable by accounts with 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 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 Update the vesting contract address. - * @param _vestingContract New vesting contract address. - * - * @dev Only callable by accounts with SETTER_ROLE. Cannot be zero address. - */ - function setVestingContract(address _vestingContract) external onlyRole(SETTER_ROLE) { - if (_vestingContract == address(0)) { - revert ZeroAddress(); - } - vestingContract = IVesting(_vestingContract); - emit VestingContractUpdated(_vestingContract); - } - - /** - * @notice Rescue tokens accidentally sent to the contract. - * @param token Address of the token to rescue. - * @param to Recipient address for the rescued tokens. - * @param amount Amount of tokens to transfer. - * - * @dev Only callable by accounts with DEFAULT_ADMIN_ROLE for emergency recovery. - */ - function rescueTokens(address token, address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { - IERC20(token).safeTransfer(to, amount); - } - - /* ────────────────────────── View Functions ────────────────────────── */ - - /** - * @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 active unlock requests for a specific NFT. - * @param tokenId ID of the NFT to query. - * @return Array of active UnlockRequest structs (excluding completed/vesting requests). - * - * @dev Filters out requests that have started vesting or been completed. - */ - function getActiveUnlockRequests(uint256 tokenId) external view returns (UnlockRequest[] memory) { - uint256[] memory unlockIds = tokenUnlockIds[tokenId]; - uint256 activeCount = 0; - - // Count active requests (non-zero amount and not vesting) - for (uint256 i = 0; i < unlockIds.length; i++) { - if (unlockRequests[unlockIds[i]].amount > 0 && !unlockRequests[unlockIds[i]].vestingStarted) { - activeCount++; - } - } - - // Populate active requests array - UnlockRequest[] memory activeRequests = new UnlockRequest[](activeCount); - uint256 index = 0; - for (uint256 i = 0; i < unlockIds.length; i++) { - UnlockRequest storage request = unlockRequests[unlockIds[i]]; - if (request.amount > 0 && !request.vestingStarted) { - activeRequests[index++] = request; - } - } - - return activeRequests; - } - - /** - * @notice Check if an NFT has any active unlock requests. - * @param tokenId ID of the NFT to check. - * @return Whether the NFT has active unlock requests. - */ - function isUnlocking(uint256 tokenId) external view returns (bool) { - uint256[] memory unlockIds = tokenUnlockIds[tokenId]; - for (uint256 i = 0; i < unlockIds.length; i++) { - UnlockRequest storage request = unlockRequests[unlockIds[i]]; - if (request.amount > 0 && !request.vestingStarted) { - return true; - } - } - return false; - } - - /** - * @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) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return 0; - } - return request.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) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return false; - } - return block.timestamp >= request.unlockInitiatedTime + cliffDuration; - } - - /** - * @notice Get the time remaining in the cliff period for an unlock request. - * @param unlockId ID of the unlock request. - * @return Seconds remaining until cliff period ends, or 0 if passed/invalid. - */ - function getCliffTimeRemaining(uint256 unlockId) external view returns (uint256) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return 0; - } - - uint256 cliffEndTime = request.unlockInitiatedTime + cliffDuration; - if (block.timestamp >= cliffEndTime) { - return 0; - } - - return cliffEndTime - block.timestamp; - } - - /** - * @notice Returns the current version of the contract. - * @return Version string of the contract. - * @dev This function can be used to verify which version of the contract is deployed. - */ - function version() external pure returns (string memory) { - return "1.0.0"; - } -} From a25d46ecb8d8db3fd85a6fe5aae249acb4219fd8 Mon Sep 17 00:00:00 2001 From: Naveed Date: Wed, 23 Jul 2025 12:35:29 +0200 Subject: [PATCH 32/48] Add missing codes --- contracts/builders-nft/SymmioBuildersNft.sol | 4 +- .../builders-nft/SymmioBuildersNftManager.sol | 2 +- .../SymmioBuildersNftUnlockMangaer.sol | 581 ++++++++++++++++++ 3 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index 1e1b0fd..d5e4310 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -10,10 +10,10 @@ pragma solidity ^0.8.27; * The manager contract handles all lock data, unlock processes, and fee management. */ -import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; 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"; contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, PausableUpgradeable { /* ─────────────────────────────── Roles ─────────────────────────────── */ diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index f907357..c6d7769 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -10,7 +10,7 @@ pragma solidity ^0.8.27; * • SYMM token locking with burning and without burning (for MINTER_ROLE) * • Lock data management for all NFTs * • NFT merging functionality - * • Integration with unlock manager for time-locked releases + * • Time-locked unlock functionality * • Fee collector management and notifications * • Cross-chain synchronization capabilities * • Transfer restrictions based on unlock status diff --git a/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol b/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol new file mode 100644 index 0000000..56fcc8a --- /dev/null +++ b/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/** + * @title SymmUnlockManager + * @notice Manages the unlock process for SYMM tokens locked in SymmioBuildersNFTManager contracts with + * cliff periods and vesting integration. Provides a complete workflow for token unlocking + * including request initiation, cliff enforcement, cancellation capabilities, and vesting setup. + * + * @dev Core features include: + * • Unlock request management with unique ID tracking + * • Configurable cliff period enforcement before token release + * • Integration with external vesting contracts for gradual token release + * • Cancellation functionality for unlock requests during cliff period + * • Comprehensive tracking of unlock status and timing + * • Emergency pause functionality for security incidents + * • Token rescue capabilities for administrative recovery + * • Detailed view functions for unlock request analysis + * + * The contract coordinates between SymmioBuildersNFTManager for lock management and external + * vesting contracts for token distribution, ensuring secure and controlled token unlocking + * with configurable time-based restrictions and user flexibility. + * + * @dev This contract is designed to be used with OpenZeppelin's TransparentUpgradeableProxy. + */ + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/* ────────────────────────── External Interfaces ────────────────────────── */ + +/** + * @notice Interface for the SymmioBuildersNFT contract to query ownership and lock data. + */ +interface ISymmioBuildersNftManager { + /** + * @notice Get the owner of a specific NFT. + * @param tokenId ID of the NFT to query. + * @return Address of the NFT owner. + */ + function ownerOf(uint256 tokenId) external view returns (address); + + /** + * @notice Get the lock data for a specific NFT. + * @param tokenId ID of the NFT to query. + * @return amount Amount of tokens locked. + * @return lockTimestamp Timestamp when tokens were locked. + * @return brandName Brand name associated with the NFT. + * @return unlockingAmount Amount currently being unlocked. + */ + function lockData( + uint256 tokenId + ) external view returns (uint256 amount, uint256 lockTimestamp, string memory brandName, uint256 unlockingAmount); + + /** + * @notice Complete the unlock process for an NFT. + * @param tokenId ID of the NFT to unlock. + * @param amount Amount of tokens to unlock. + */ + function completeUnlock(uint256 tokenId, uint256 amount) external; + + /** + * @notice Cancel an unlock process for an NFT. + * @param tokenId ID of the NFT to cancel unlock for. + * @param amount Amount to cancel from the unlock process. + */ + function cancelUnlock(uint256 tokenId, uint256 amount) external; +} + +/** + * @notice Interface for the Vesting contract to set up vesting plans. + */ +interface IVesting { + /** + * @notice 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 user. + */ + function setupVestingPlans(address token, uint256 startTime, uint256 endTime, address[] memory users, uint256[] memory amounts) external; +} + +contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { + using SafeERC20 for IERC20; + + /* ─────────────────────────────── Roles ─────────────────────────────── */ + + /// @notice Role for updating configuration parameters like cliff and vesting durations. + bytes32 public constant SETTER_ROLE = keccak256("SETTER_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 The SymmioBuildersNFT contract. + ISymmioBuildersNftManager public symmBuildersNftManager; + + /// @notice The Vesting contract for managing token vesting plans. + IVesting public vestingContract; + + /// @notice The SYMM token contract. + IERC20 public SYMM; + + /// @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 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; + + /// @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; + + /* ─────────────────────────────── 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. + */ + struct UnlockRequest { + uint256 amount; + uint256 unlockInitiatedTime; + address owner; + uint256 tokenId; + bool cliffPassed; + bool vestingStarted; + } + + /* ─────────────────────────────── Events ─────────────────────────────── */ + + /** + * @notice Emitted when an unlock request is initiated. + * @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 request 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 the cliff period for an unlock request is completed. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + */ + event CliffCompleted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner); + + /** + * @notice Emitted when vesting starts for an unlock request. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens entering vesting. + */ + event VestingStarted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount); + + /** + * @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 the vesting contract address is updated. + * @param newVestingContract New vesting contract address. + */ + event VestingContractUpdated(address newVestingContract); + + /* ─────────────────────────────── Errors ─────────────────────────────── */ + + error NotNFTOwner(); // caller is not the owner of the NFT + error UnlockNotFound(); // unlock request ID is invalid or not found + error CliffNotPassed(); // cliff period has not yet passed + error VestingAlreadyStarted(); // vesting has already started for this unlock request + error InvalidDuration(); // invalid duration (zero) provided for cliff or vesting + error ZeroAddress(); // zero address provided for critical parameters + error ZeroAmount(); // zero amount provided for operations requiring non-zero value + error UnauthorizedAccess(address caller, address requiredCaller); // unauthorized caller attempted restricted action + + /* ─────────────────────────── Initialization ─────────────────────────── */ + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the SymmUnlockManager with core contracts and configuration. + * @param _symmBuildersNft Address of the SymmioBuildersNFT contract. + * @param _symm Address of the SYMM token contract. + * @param _vestingContract Address of the Vesting contract. + * @param _admin Address to receive admin and all role assignments. + * @param _cliffDuration Duration of the cliff period in seconds. + * @param _vestingDuration Duration of the vesting period in seconds. + * + * @dev Sets up access control and validates all inputs. Reverts on zero addresses or invalid durations. + * This replaces the constructor for upgradeable contracts. + */ + function initialize( + address _symmBuildersNft, + address _symm, + address _vestingContract, + address _admin, + uint256 _cliffDuration, + uint256 _vestingDuration + ) public initializer { + if (_symmBuildersNft == address(0) || _symm == address(0) || _vestingContract == address(0) || _admin == address(0)) { + revert ZeroAddress(); + } + if (_cliffDuration == 0 || _vestingDuration == 0) { + revert InvalidDuration(); + } + + // Initialize parent contracts + __AccessControlEnumerable_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + + // Set contract addresses and parameters + symmBuildersNftManager = ISymmioBuildersNftManager(_symmBuildersNft); + SYMM = IERC20(_symm); + vestingContract = IVesting(_vestingContract); + cliffDuration = _cliffDuration; + vestingDuration = _vestingDuration; + + // Initialize counter + _unlockIdCounter = 0; + + // Grant roles to admin + _grantRole(DEFAULT_ADMIN_ROLE, _admin); + _grantRole(SETTER_ROLE, _admin); + _grantRole(PAUSER_ROLE, _admin); + _grantRole(UNPAUSER_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(); + } + + /* ──────────────────── Unlock Management ──────────────────── */ + + /** + * @notice Initiate an unlock request for an NFT (called by SymmioBuildersNFT contract). + * @param tokenId ID of the NFT to unlock. + * @param owner Owner of the NFT. + * @param amount Amount of tokens to unlock. + * + * @dev Creates a new unlock request with cliff period enforcement. + * Only callable by the SymmioBuildersNFT contract. + */ + function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external whenNotPaused { + if (msg.sender != address(symmBuildersNftManager)) { + revert UnauthorizedAccess(msg.sender, address(symmBuildersNftManager)); + } + if (amount == 0) { + revert ZeroAmount(); + } + + uint256 unlockId = _unlockIdCounter++; + unlockRequests[unlockId] = UnlockRequest({ + amount: amount, + unlockInitiatedTime: block.timestamp, + owner: owner, + tokenId: tokenId, + cliffPassed: false, + vestingStarted: false + }); + + tokenUnlockIds[tokenId].push(unlockId); + + emit UnlockInitiated(unlockId, tokenId, owner, 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 notifies the 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 (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { + revert NotNFTOwner(); + } + if (request.cliffPassed) { + revert CliffNotPassed(); + } + + 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; + } + } + + // Notify NFT contract to cancel the unlock + symmBuildersNftManager.cancelUnlock(tokenId, 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 Transfers tokens to vesting contract and sets up 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 (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { + revert NotNFTOwner(); + } + if (request.vestingStarted) { + revert VestingAlreadyStarted(); + } + if (block.timestamp < request.unlockInitiatedTime + cliffDuration) { + revert CliffNotPassed(); + } + + // Mark cliff as passed and vesting as started + request.cliffPassed = true; + request.vestingStarted = true; + + // Complete unlock on NFT contract + symmBuildersNftManager.completeUnlock(request.tokenId, request.amount); + + // Set up vesting plan for the owner + address[] memory users = new address[](1); + users[0] = request.owner; + uint256[] memory amounts = new uint256[](1); + amounts[0] = request.amount; + + // Approve vesting contract to transfer tokens + SYMM.approve(address(vestingContract), request.amount); + + // Set up vesting plan starting from now + vestingContract.setupVestingPlans(address(SYMM), block.timestamp, block.timestamp + vestingDuration, users, amounts); + + emit CliffCompleted(unlockId, request.tokenId, request.owner); + emit VestingStarted(unlockId, request.tokenId, request.owner, request.amount); + } + + /* ────────────────────────── Admin Functions ────────────────────────── */ + + /** + * @notice Update the cliff duration for new unlock requests. + * @param _cliffDuration New cliff duration in seconds. + * + * @dev Only callable by accounts with 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 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 Update the vesting contract address. + * @param _vestingContract New vesting contract address. + * + * @dev Only callable by accounts with SETTER_ROLE. Cannot be zero address. + */ + function setVestingContract(address _vestingContract) external onlyRole(SETTER_ROLE) { + if (_vestingContract == address(0)) { + revert ZeroAddress(); + } + vestingContract = IVesting(_vestingContract); + emit VestingContractUpdated(_vestingContract); + } + + /** + * @notice Rescue tokens accidentally sent to the contract. + * @param token Address of the token to rescue. + * @param to Recipient address for the rescued tokens. + * @param amount Amount of tokens to transfer. + * + * @dev Only callable by accounts with DEFAULT_ADMIN_ROLE for emergency recovery. + */ + function rescueTokens(address token, address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { + IERC20(token).safeTransfer(to, amount); + } + + /* ────────────────────────── View Functions ────────────────────────── */ + + /** + * @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 active unlock requests for a specific NFT. + * @param tokenId ID of the NFT to query. + * @return Array of active UnlockRequest structs (excluding completed/vesting requests). + * + * @dev Filters out requests that have started vesting or been completed. + */ + function getActiveUnlockRequests(uint256 tokenId) external view returns (UnlockRequest[] memory) { + uint256[] memory unlockIds = tokenUnlockIds[tokenId]; + uint256 activeCount = 0; + + // Count active requests (non-zero amount and not vesting) + for (uint256 i = 0; i < unlockIds.length; i++) { + if (unlockRequests[unlockIds[i]].amount > 0 && !unlockRequests[unlockIds[i]].vestingStarted) { + activeCount++; + } + } + + // Populate active requests array + UnlockRequest[] memory activeRequests = new UnlockRequest[](activeCount); + uint256 index = 0; + for (uint256 i = 0; i < unlockIds.length; i++) { + UnlockRequest storage request = unlockRequests[unlockIds[i]]; + if (request.amount > 0 && !request.vestingStarted) { + activeRequests[index++] = request; + } + } + + return activeRequests; + } + + /** + * @notice Check if an NFT has any active unlock requests. + * @param tokenId ID of the NFT to check. + * @return Whether the NFT has active unlock requests. + */ + function isUnlocking(uint256 tokenId) external view returns (bool) { + uint256[] memory unlockIds = tokenUnlockIds[tokenId]; + for (uint256 i = 0; i < unlockIds.length; i++) { + UnlockRequest storage request = unlockRequests[unlockIds[i]]; + if (request.amount > 0 && !request.vestingStarted) { + return true; + } + } + return false; + } + + /** + * @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) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return 0; + } + return request.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) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return false; + } + return block.timestamp >= request.unlockInitiatedTime + cliffDuration; + } + + /** + * @notice Get the time remaining in the cliff period for an unlock request. + * @param unlockId ID of the unlock request. + * @return Seconds remaining until cliff period ends, or 0 if passed/invalid. + */ + function getCliffTimeRemaining(uint256 unlockId) external view returns (uint256) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return 0; + } + + uint256 cliffEndTime = request.unlockInitiatedTime + cliffDuration; + if (block.timestamp >= cliffEndTime) { + return 0; + } + + return cliffEndTime - block.timestamp; + } + + /** + * @notice Returns the current version of the contract. + * @return Version string of the contract. + * @dev This function can be used to verify which version of the contract is deployed. + */ + function version() external pure returns (string memory) { + return "1.0.0"; + } +} From dbc7cdab1c7120d7a1d7da53ead490c0bb3f251a Mon Sep 17 00:00:00 2001 From: Naveed Date: Wed, 23 Jul 2025 14:24:05 +0200 Subject: [PATCH 33/48] Move lockData to nft --- contracts/builders-nft/SymmioBuildersNft.sol | 162 +++++++++--- .../builders-nft/SymmioBuildersNftManager.sol | 246 +++++------------- .../SymmioBuildersNftUnlockMangaer.sol | 57 +--- .../ISymmBuildersNftUnlockManager.sol | 8 + .../interfaces/ISymmioBuildersNft.sol | 29 +++ .../interfaces/ISymmioBuildersNftManager.sol | 8 + 6 files changed, 239 insertions(+), 271 deletions(-) create mode 100644 contracts/builders-nft/interfaces/ISymmBuildersNftUnlockManager.sol create mode 100644 contracts/builders-nft/interfaces/ISymmioBuildersNft.sol create mode 100644 contracts/builders-nft/interfaces/ISymmioBuildersNftManager.sol diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index d5e4310..9f6c012 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -14,8 +14,16 @@ 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"; - -contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, PausableUpgradeable { +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. @@ -30,13 +38,19 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access /// @notice Role for unpausing the contract operations. bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); + /// @notice Role for pausing/unpausing NFT transfers specifically. + bytes32 public constant TRANSFER_PAUSER_ROLE = keccak256("TRANSFER_PAUSER_ROLE"); + /* ──────────────────────── Storage Variables ──────────────────────── */ /// @notice Counter for generating unique token IDs sequentially. uint256 private _tokenIdCounter; - /// @notice Mapping of token ID to its brand name. - mapping(uint256 => string) public brandNames; + /// @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; @@ -47,21 +61,32 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access * @notice Emitted when an NFT is minted. * @param to Address receiving the NFT. * @param tokenId ID of the minted NFT. - * @param brandName Brand name associated with the 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 NFTMinted(address indexed to, uint256 indexed tokenId, string brandName); + event LockDataUpdated(uint256 indexed tokenId, uint256 amount, uint256 unlockingAmount, string name); /** - * @notice Emitted when an NFT's brand name is updated. - * @param tokenId ID of the NFT. - * @param newBrandName New brand name assigned. + * @notice Emitted when the transfer pause state is updated. + * @param paused New pause state (true for paused, false for unpaused). */ - event BrandNameUpdated(uint256 indexed tokenId, string newBrandName); + 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 ─────────────────────────── */ @@ -73,12 +98,11 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access /** * @notice Initialize the upgradeable SymmioBuildersNft contract. * @param _admin Address to receive admin and all role assignments. - * @param _manager Address of the manager contract to receive minter/burner roles. * * @dev Sets up the ERC721 contract and assigns roles. */ - function initialize(address _admin, address _manager) public initializer { - if (_admin == address(0) || _manager == address(0)) revert ZeroAddress(); + function initialize(address _admin) public initializer { + if (_admin == address(0)) revert ZeroAddress(); // Initialize parent contracts __ERC721_init("Symmio Builders NFT", "BUILDERS"); @@ -90,10 +114,6 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access _grantRole(DEFAULT_ADMIN_ROLE, _admin); _grantRole(PAUSER_ROLE, _admin); _grantRole(UNPAUSER_ROLE, _admin); - - // Grant minter and burner roles to manager - _grantRole(MINTER_ROLE, _manager); - _grantRole(BURNER_ROLE, _manager); } /* ────────────────────────── Core Functions ────────────────────────── */ @@ -101,37 +121,37 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access /** * @notice Mint a new NFT with a brand name. * @param to Address to mint the NFT to. - * @param brandName Brand name for the NFT. + * @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, string memory brandName) external onlyRole(MINTER_ROLE) whenNotPaused returns (uint256 tokenId) { + function mint(address to, uint256 amount, string memory name) external onlyRole(MINTER_ROLE) whenNotPaused returns (uint256 tokenId) { tokenId = _tokenIdCounter++; _safeMint(to, tokenId); - brandNames[tokenId] = brandName; + lockData[tokenId] = ISymmioBuildersNft.LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0, name: name }); - emit NFTMinted(to, tokenId, brandName); + emit NFTMinted(to, tokenId, amount, name); } /** * @notice Mint a specific NFT ID with a brand name (used for cross-chain sync). * @param to Address to mint the NFT to. * @param tokenId Specific token ID to mint. - * @param brandName Brand name for the NFT. + * @param amount Amount of SYMM tokens to lock. + * @param name Name for the NFT. * * @dev Only callable by addresses with MINTER_ROLE. Updates counter to avoid conflicts. */ - function mintWithId(address to, uint256 tokenId, string memory brandName) external onlyRole(MINTER_ROLE) whenNotPaused { + function mintWithId(address to, uint256 tokenId, uint256 amount, string memory name) external onlyRole(MINTER_ROLE) whenNotPaused { // Update token ID counter to avoid conflicts - if (tokenId >= _tokenIdCounter) { - _tokenIdCounter = tokenId + 1; - } + if (tokenId >= _tokenIdCounter) _tokenIdCounter = tokenId + 1; _safeMint(to, tokenId); - brandNames[tokenId] = brandName; + lockData[tokenId] = ISymmioBuildersNft.LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0, name: name }); - emit NFTMinted(to, tokenId, brandName); + emit NFTMinted(to, tokenId, amount, name); } /** @@ -140,28 +160,53 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access * * @dev Only callable by addresses with BURNER_ROLE (typically the manager contract). */ - function burn(uint256 tokenId) external onlyRole(BURNER_ROLE) { + function burn(uint256 tokenId) external onlyRole(BURNER_ROLE) whenNotPaused { _burn(tokenId); - delete brandNames[tokenId]; + delete lockData[tokenId]; } /** - * @notice Update the brand name of an NFT. + * @notice Update the lock data of an NFT. * @param tokenId ID of the NFT to update. - * @param newBrandName New brand name for the NFT. + * @param amount Amount of SYMM tokens locked. + * @param name Name associated with the NFT. * * @dev Only callable by the NFT owner. */ - function updateBrandName(uint256 tokenId, string memory newBrandName) external { + function updateLockData(uint256 tokenId, uint256 amount, uint256 unlockingAmount, string memory name) external onlyRole(MINTER_ROLE) { if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); - brandNames[tokenId] = newBrandName; + lockData[tokenId] = ISymmioBuildersNft.LockData({ + amount: amount, + lockTimestamp: block.timestamp, + unlockingAmount: unlockingAmount, + name: name + }); - emit BrandNameUpdated(tokenId, newBrandName); + 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. @@ -176,6 +221,20 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access 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 ───────────────────────── */ /** @@ -194,12 +253,39 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access _unpause(); } + /** + * @notice Set the pause state for NFT transfers independently of contract pause. + * @param _paused True to pause transfers, false to unpause. + * + * @dev Only callable by accounts with TRANSFER_PAUSER_ROLE. + */ + function setTransfersPaused(bool _paused) external onlyRole(TRANSFER_PAUSER_ROLE) { + transfersPaused = _paused; + emit TransfersPausedUpdated(_paused); + } + /* ───────────────────────── Internal Overrides ───────────────────────── */ /** - * @dev Override _update to add pause check for transfers. + * @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 whenNotPaused returns (address) { + 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); } @@ -212,7 +298,7 @@ contract SymmioBuildersNft is Initializable, ERC721EnumerableUpgradeable, Access */ function supportsInterface( bytes4 interfaceId - ) public view override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable) returns (bool) { + ) public view override(ERC721EnumerableUpgradeable, AccessControlEnumerableUpgradeable, IERC165) returns (bool) { return super.supportsInterface(interfaceId); } diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index c6d7769..1a632cf 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -18,12 +18,16 @@ pragma solidity ^0.8.27; * This contract acts as the central logic hub while the NFT contract remains simple. */ -import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; + +import "./interfaces/ISymmioBuildersNft.sol"; +import "./interfaces/ISymmBuildersNftUnlockManager.sol"; +import "./interfaces/ISymmioBuildersNftManager.sol"; /* ────────────────────────── External Interfaces ────────────────────────── */ @@ -32,30 +36,6 @@ interface IERC20Burnable is IERC20 { function burnFrom(address account, uint256 amount) external; } -/** - * @notice Interface for the SymmioBuildersNft contract. - */ -interface ISymmioBuildersNft { - function mint(address to, string memory brandName) external returns (uint256 tokenId); - - function mintWithId(address to, uint256 tokenId, string memory brandName) external; - - function burn(uint256 tokenId) external; - - function ownerOf(uint256 tokenId) external view returns (address); - - function brandNames(uint256 tokenId) external view returns (string memory); -} - -/** - * @notice Interface for the unlock manager contract handling token unlock processes. - */ -interface ISymmUnlockManager { - function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external; - - function isUnlocking(uint256 tokenId) external view returns (bool); -} - /** * @notice Interface for the fee collector contract handling fee collection. */ @@ -63,7 +43,7 @@ interface ISymmFeeCollector { function onLockedAmountChanged(int256 amount) external; } -contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { +contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, ISymmioBuildersNftManager { using SafeERC20 for IERC20; /* ─────────────────────────────── Roles ─────────────────────────────── */ @@ -80,9 +60,6 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra /// @notice Role for unpausing the contract operations. bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); - /// @notice Role for pausing/unpausing NFT transfers specifically. - bytes32 public constant TRANSFER_PAUSER_ROLE = keccak256("TRANSFER_PAUSER_ROLE"); - /// @notice Role for syncing cross-chain lock data and minting NFTs. bytes32 public constant SYNC_ROLE = keccak256("SYNC_ROLE"); @@ -95,17 +72,11 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra ISymmioBuildersNft public nftContract; /// @notice The unlock manager contract for handling token unlock processes. - ISymmUnlockManager public unlockManager; + ISymmBuildersNftUnlockManager public unlockManager; /// @notice The minimum amount of SYMM tokens required to mint an NFT. uint256 public minLockAmount; - /// @notice Flag indicating whether NFT transfers are paused. - bool public transfersPaused; - - /// @notice Mapping of token ID to its comprehensive lock data. - mapping(uint256 => LockData) public lockData; - /// @notice Mapping of token ID to its related fee collector addresses. mapping(uint256 => address[]) public tokenRelatedFeeCollectors; @@ -114,14 +85,22 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra /* ─────────────────────────────── 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. - * @param brandName Brand name associated with the NFT. */ - event TokenLocked(address indexed user, uint256 indexed tokenId, uint256 amount, string brandName); + event TokenLocked(address indexed user, uint256 indexed tokenId, uint256 amount); /** * @notice Emitted when an NFT is minted without burning SYMM. @@ -129,9 +108,9 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra * @param to Address receiving the NFT. * @param tokenId ID of the minted NFT. * @param amount Amount associated with the NFT. - * @param brandName Brand name 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 brandName); + event NFTMintedWithoutBurn(address indexed minter, address indexed to, uint256 indexed tokenId, uint256 amount, string name); /** * @notice Emitted when two NFTs are merged into one. @@ -149,6 +128,14 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra */ event UnlockInitiated(uint256 indexed tokenId, address indexed owner, uint256 amount); + /** + * @notice Emitted when an unlock process is completed for an NFT. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens to unlock. + */ + event UnlockCompleted(uint256 indexed tokenId, address indexed owner, uint256 amount); + /** * @notice Emitted when the minimum lock amount is updated. * @param newMinAmount New minimum lock amount. @@ -161,12 +148,6 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra */ event UnlockManagerUpdated(address newUnlockManager); - /** - * @notice Emitted when the transfer pause state is updated. - * @param paused New pause state (true for paused, false for unpaused). - */ - event TransfersPausedUpdated(bool paused); - /** * @notice Emitted when an NFT is minted for cross-chain synchronization. * @param to Address receiving the NFT. @@ -198,38 +179,11 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra error InvalidTokenId(); error ZeroAddress(); error ZeroAmount(); - error TransfersPaused(); error UnlockManagerNotSet(); error TokenHasActiveUnlock(); error UnauthorizedAccess(address caller, address requiredCaller); error LengthMismatch(); - /* ─────────────────────────────── Structs ─────────────────────────────── */ - - /** - * @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; - } - - /* ─────────────────────────────── Modifiers ─────────────────────────────── */ - - /** - * @notice Ensure transfers are not paused and token has no active unlock. - * @param tokenId ID of the token to check. - */ - modifier transfersAllowed(uint256 tokenId) { - if (transfersPaused) revert TransfersPaused(); - if (lockData[tokenId].unlockingAmount > 0) revert TokenHasActiveUnlock(); - _; - } - /* ─────────────────────────── Initialization ─────────────────────────── */ /// @custom:oz-upgrades-unsafe-allow constructor @@ -264,7 +218,6 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra _grantRole(SETTER_ROLE, _admin); _grantRole(PAUSER_ROLE, _admin); _grantRole(UNPAUSER_ROLE, _admin); - _grantRole(TRANSFER_PAUSER_ROLE, _admin); _grantRole(SYNC_ROLE, _admin); } @@ -285,15 +238,12 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra SYMM.burnFrom(msg.sender, amount); // Mint new NFT - tokenId = nftContract.mint(msg.sender, brandName); - - // Store lock data - lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0 }); + tokenId = nftContract.mint(msg.sender, amount, brandName); // Notify fee collectors _notifyFeeCollectors(tokenId, int256(amount)); - emit TokenLocked(msg.sender, tokenId, amount, brandName); + emit NFTMinted(msg.sender, tokenId, amount, brandName); } /** @@ -313,10 +263,7 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra if (amount < minLockAmount) revert AmountBelowMinimum(amount, minLockAmount); // Mint new NFT - tokenId = nftContract.mint(to, brandName); - - // Store lock data (same as regular mint) - lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0 }); + tokenId = nftContract.mint(to, amount, brandName); // Notify fee collectors _notifyFeeCollectors(tokenId, int256(amount)); @@ -336,12 +283,13 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra SYMM.burnFrom(msg.sender, amount); // Increase the locked amount - lockData[tokenId].amount += 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, nftContract.brandNames(tokenId)); + emit TokenLocked(msg.sender, tokenId, amount); } /* ────────────────────────── NFT Management ────────────────────────── */ @@ -355,23 +303,22 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra if (nftContract.ownerOf(targetTokenId) != msg.sender) revert NotTokenOwner(); if (nftContract.ownerOf(sourceTokenId) != msg.sender) revert NotTokenOwner(); - LockData storage targetData = lockData[targetTokenId]; - LockData storage sourceData = lockData[sourceTokenId]; + ISymmioBuildersNft.LockData memory targetData = nftContract.getLockData(targetTokenId); + ISymmioBuildersNft.LockData memory sourceData = nftContract.getLockData(sourceTokenId); if (targetData.unlockingAmount > 0 || sourceData.unlockingAmount > 0) revert TokenHasActiveUnlock(); // Merge locked amounts uint256 newAmount = targetData.amount + sourceData.amount; - targetData.amount = newAmount; + nftContract.updateLockData(targetTokenId, newAmount, targetData.unlockingAmount, targetData.name); + + // Burn the source NFT and clear its data + nftContract.burn(sourceTokenId); // 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); - delete lockData[sourceTokenId]; - emit TokensMerged(targetTokenId, sourceTokenId, newAmount); } @@ -386,14 +333,14 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra if (nftContract.ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); if (address(unlockManager) == address(0)) revert UnlockManagerNotSet(); - LockData storage data = lockData[tokenId]; + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); uint256 availableAmount = data.amount - data.unlockingAmount; if (amount > availableAmount) revert InsufficientLockedAmount(); if (amount == 0) revert ZeroAmount(); // Update the unlocking amount - data.unlockingAmount += amount; + nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount + amount, data.name); // Delegate to the unlock manager unlockManager.initiateUnlock(tokenId, msg.sender, amount); @@ -414,15 +361,13 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra function completeUnlock(uint256 tokenId, uint256 amount) external { if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); - LockData storage data = lockData[tokenId]; - data.unlockingAmount -= amount; - data.amount -= amount; + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); + nftContract.updateLockData(tokenId, data.amount - amount, data.unlockingAmount - amount, data.name); // Burn the NFT if no locked tokens remain - if (data.amount == 0) { - nftContract.burn(tokenId); - delete lockData[tokenId]; - } + if (data.amount == 0) nftContract.burn(tokenId); + + emit UnlockCompleted(tokenId, msg.sender, amount); } /** @@ -435,7 +380,8 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra function cancelUnlock(uint256 tokenId, uint256 amount) external { if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); - lockData[tokenId].unlockingAmount -= amount; + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); + nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount - amount, data.name); // Notify fee collectors _notifyFeeCollectors(tokenId, int256(amount)); @@ -448,19 +394,16 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra * @param to Address to mint the NFT to. * @param tokenId Specific token ID to mint. * @param amount Amount of SYMM tokens locked. - * @param brandName Brand name for the NFT. + * @param name Brand name for the NFT. */ - function syncMint(address to, uint256 tokenId, uint256 amount, string memory brandName) external onlyRole(SYNC_ROLE) whenNotPaused { + function syncMint(address to, uint256 tokenId, uint256 amount, string memory name) external onlyRole(SYNC_ROLE) whenNotPaused { // Mint NFT with specific ID - nftContract.mintWithId(to, tokenId, brandName); - - // Store lock data - lockData[tokenId] = LockData({ amount: amount, lockTimestamp: block.timestamp, unlockingAmount: 0 }); + nftContract.mintWithId(to, tokenId, amount, name); // Notify fee collectors _notifyFeeCollectors(tokenId, int256(amount)); - emit SyncMint(to, tokenId, amount, brandName); + emit SyncMint(to, tokenId, amount, name); } /** @@ -468,46 +411,19 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra * @param tokenIds Array of token IDs to update. * @param lockDatas Array of lock data to apply. */ - function batchUpdateLockData(uint256[] calldata tokenIds, LockData[] calldata lockDatas) external onlyRole(SYNC_ROLE) { + 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 = lockData[tokenIds[i]].amount; + uint256 oldAmount = nftContract.getLockData(tokenIds[i]).amount; uint256 newAmount = lockDatas[i].amount; - lockData[tokenIds[i]] = lockDatas[i]; + 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)); } } - /* ───────────────────────── Transfer Controls ───────────────────────── */ - - /** - * @notice Check if an NFT transfer is allowed. - * @param tokenId ID of the NFT to check. - * @return Whether the transfer is allowed. - */ - function isTransferAllowed(uint256 tokenId) external view returns (bool) { - return !transfersPaused && lockData[tokenId].unlockingAmount == 0; - } - - /** - * @notice Hook called before NFT transfers to check restrictions. - * @param from Address transferring from. - * @param to Address transferring to. - * @param tokenId ID of the NFT being transferred. - * - * @dev Should be called by the NFT contract before transfers. - */ - function beforeTokenTransfer(address from, address to, uint256 tokenId) external view { - // Skip checks for minting (from == address(0)) and burning (to == address(0)) - if (from != address(0) && to != address(0)) { - if (transfersPaused) revert TransfersPaused(); - if (lockData[tokenId].unlockingAmount > 0) revert TokenHasActiveUnlock(); - } - } - /* ───────────────────────── Pause Controls ───────────────────────── */ /** @@ -524,15 +440,6 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra _unpause(); } - /** - * @notice Set the pause state for NFT transfers. - * @param _paused True to pause transfers, false to unpause. - */ - function setTransfersPaused(bool _paused) external onlyRole(TRANSFER_PAUSER_ROLE) { - transfersPaused = _paused; - emit TransfersPausedUpdated(_paused); - } - /* ────────────────────────── Admin Functions ────────────────────────── */ /** @@ -551,7 +458,7 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra */ function setUnlockManager(address _unlockManager) external onlyRole(SETTER_ROLE) { if (_unlockManager == address(0)) revert ZeroAddress(); - unlockManager = ISymmUnlockManager(_unlockManager); + unlockManager = ISymmBuildersNftUnlockManager(_unlockManager); emit UnlockManagerUpdated(_unlockManager); } @@ -586,41 +493,6 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra /* ────────────────────────── View Functions ────────────────────────── */ - /** - * @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 lock data for multiple NFTs in a single call. - * @param tokenIds Array of token IDs to query. - * @return Array of LockData structs. - */ - function getLockDataBatch(uint256[] calldata tokenIds) external view returns (LockData[] memory) { - LockData[] memory result = new LockData[](tokenIds.length); - for (uint256 i = 0; i < tokenIds.length; i++) { - result[i] = lockData[tokenIds[i]]; - } - return result; - } - - /** - * @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) { - // This would need to iterate through user's NFTs from the NFT contract - // Implementation depends on how the NFT contract exposes user's tokens - // For now, returning 0 as placeholder - return 0; - } - /** * @notice Get all fee collectors for a specific NFT. * @param tokenId ID of the NFT. diff --git a/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol b/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol index 56fcc8a..3055855 100644 --- a/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol +++ b/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol @@ -26,50 +26,15 @@ pragma solidity ^0.8.27; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; -/* ────────────────────────── External Interfaces ────────────────────────── */ - -/** - * @notice Interface for the SymmioBuildersNFT contract to query ownership and lock data. - */ -interface ISymmioBuildersNftManager { - /** - * @notice Get the owner of a specific NFT. - * @param tokenId ID of the NFT to query. - * @return Address of the NFT owner. - */ - function ownerOf(uint256 tokenId) external view returns (address); - - /** - * @notice Get the lock data for a specific NFT. - * @param tokenId ID of the NFT to query. - * @return amount Amount of tokens locked. - * @return lockTimestamp Timestamp when tokens were locked. - * @return brandName Brand name associated with the NFT. - * @return unlockingAmount Amount currently being unlocked. - */ - function lockData( - uint256 tokenId - ) external view returns (uint256 amount, uint256 lockTimestamp, string memory brandName, uint256 unlockingAmount); - - /** - * @notice Complete the unlock process for an NFT. - * @param tokenId ID of the NFT to unlock. - * @param amount Amount of tokens to unlock. - */ - function completeUnlock(uint256 tokenId, uint256 amount) external; +import "./interfaces/ISymmioBuildersNft.sol"; +import "./interfaces/ISymmioBuildersNftManager.sol"; - /** - * @notice Cancel an unlock process for an NFT. - * @param tokenId ID of the NFT to cancel unlock for. - * @param amount Amount to cancel from the unlock process. - */ - function cancelUnlock(uint256 tokenId, uint256 amount) external; -} +/* ────────────────────────── External Interfaces ────────────────────────── */ /** * @notice Interface for the Vesting contract to set up vesting plans. @@ -336,9 +301,9 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, if (request.amount == 0) { revert UnlockNotFound(); } - if (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { - revert NotNFTOwner(); - } + // if (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { + // revert NotNFTOwner(); + // } if (request.cliffPassed) { revert CliffNotPassed(); } @@ -378,9 +343,9 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, if (request.amount == 0) { revert UnlockNotFound(); } - if (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { - revert NotNFTOwner(); - } + // if (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { + // revert NotNFTOwner(); + // } if (request.vestingStarted) { revert VestingAlreadyStarted(); } diff --git a/contracts/builders-nft/interfaces/ISymmBuildersNftUnlockManager.sol b/contracts/builders-nft/interfaces/ISymmBuildersNftUnlockManager.sol new file mode 100644 index 0000000..a3e46a0 --- /dev/null +++ b/contracts/builders-nft/interfaces/ISymmBuildersNftUnlockManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface ISymmBuildersNftUnlockManager { + function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external; + + function isUnlocking(uint256 tokenId) external view returns (bool); +} diff --git a/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol b/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol new file mode 100644 index 0000000..c060f13 --- /dev/null +++ b/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol @@ -0,0 +1,29 @@ +// 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 mintWithId(address to, uint256 tokenId, uint256 amount, string memory name) external; + + 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/builders-nft/interfaces/ISymmioBuildersNftManager.sol b/contracts/builders-nft/interfaces/ISymmioBuildersNftManager.sol new file mode 100644 index 0000000..fc6c6ae --- /dev/null +++ b/contracts/builders-nft/interfaces/ISymmioBuildersNftManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface ISymmioBuildersNftManager { + function completeUnlock(uint256 tokenId, uint256 amount) external; + + function cancelUnlock(uint256 tokenId, uint256 amount) external; +} From a7f099680e1db6fb364d804be68d143802554314 Mon Sep 17 00:00:00 2001 From: Naveed Date: Wed, 23 Jul 2025 14:34:42 +0200 Subject: [PATCH 34/48] Fix unlock manager --- .../builders-nft/SymmioBuildersNftManager.sol | 6 +- .../SymmioBuildersNftUnlockMangaer.sol | 205 ++++++------------ ...ol => ISymmioBuildersNftUnlockManager.sol} | 2 +- 3 files changed, 68 insertions(+), 145 deletions(-) rename contracts/builders-nft/interfaces/{ISymmBuildersNftUnlockManager.sol => ISymmioBuildersNftUnlockManager.sol} (82%) diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 1a632cf..5297548 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -26,7 +26,7 @@ import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; import "./interfaces/ISymmioBuildersNft.sol"; -import "./interfaces/ISymmBuildersNftUnlockManager.sol"; +import "./interfaces/ISymmioBuildersNftUnlockManager.sol"; import "./interfaces/ISymmioBuildersNftManager.sol"; /* ────────────────────────── External Interfaces ────────────────────────── */ @@ -72,7 +72,7 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra ISymmioBuildersNft public nftContract; /// @notice The unlock manager contract for handling token unlock processes. - ISymmBuildersNftUnlockManager public unlockManager; + ISymmioBuildersNftUnlockManager public unlockManager; /// @notice The minimum amount of SYMM tokens required to mint an NFT. uint256 public minLockAmount; @@ -458,7 +458,7 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra */ function setUnlockManager(address _unlockManager) external onlyRole(SETTER_ROLE) { if (_unlockManager == address(0)) revert ZeroAddress(); - unlockManager = ISymmBuildersNftUnlockManager(_unlockManager); + unlockManager = ISymmioBuildersNftUnlockManager(_unlockManager); emit UnlockManagerUpdated(_unlockManager); } diff --git a/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol b/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol index 3055855..248f427 100644 --- a/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol +++ b/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol @@ -4,75 +4,36 @@ pragma solidity ^0.8.27; /** * @title SymmUnlockManager * @notice Manages the unlock process for SYMM tokens locked in SymmioBuildersNFTManager contracts with - * cliff periods and vesting integration. Provides a complete workflow for token unlocking - * including request initiation, cliff enforcement, cancellation capabilities, and vesting setup. + * integrated cliff periods and sophisticated vesting functionality. Inherits from VestingV2 to + * provide comprehensive token vesting with penalty mechanisms and flexible claiming options. * * @dev Core features include: * • Unlock request management with unique ID tracking * • Configurable cliff period enforcement before token release - * • Integration with external vesting contracts for gradual token release + * • Full VestingV2 functionality inherited (linear vesting, penalties, percentage claims) * • Cancellation functionality for unlock requests during cliff period * • Comprehensive tracking of unlock status and timing * • Emergency pause functionality for security incidents * • Token rescue capabilities for administrative recovery * • Detailed view functions for unlock request analysis * - * The contract coordinates between SymmioBuildersNFTManager for lock management and external - * vesting contracts for token distribution, ensuring secure and controlled token unlocking - * with configurable time-based restrictions and user flexibility. + * The contract coordinates with SymmioBuildersNFTManager for lock management and uses + * inherited VestingV2 functionality for sophisticated token distribution with penalties, + * percentage-based claiming, and multiple vesting plans per user. * * @dev This contract is designed to be used with OpenZeppelin's TransparentUpgradeableProxy. */ -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; - +import "../vesting/VestingV2.sol"; import "./interfaces/ISymmioBuildersNft.sol"; import "./interfaces/ISymmioBuildersNftManager.sol"; -/* ────────────────────────── External Interfaces ────────────────────────── */ - -/** - * @notice Interface for the Vesting contract to set up vesting plans. - */ -interface IVesting { - /** - * @notice 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 user. - */ - function setupVestingPlans(address token, uint256 startTime, uint256 endTime, address[] memory users, uint256[] memory amounts) external; -} - -contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable { - using SafeERC20 for IERC20; - - /* ─────────────────────────────── Roles ─────────────────────────────── */ - - /// @notice Role for updating configuration parameters like cliff and vesting durations. - bytes32 public constant SETTER_ROLE = keccak256("SETTER_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 ──────────────────────── */ +contract SymmioBuildersNftUnlockManager is VestingV2 { + /* ──────────────────────── Additional Storage Variables ──────────────────────── */ /// @notice The SymmioBuildersNFT contract. ISymmioBuildersNftManager public symmBuildersNftManager; - /// @notice The Vesting contract for managing token vesting plans. - IVesting public vestingContract; - /// @notice The SYMM token contract. IERC20 public SYMM; @@ -92,7 +53,7 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, mapping(uint256 => uint256[]) public tokenUnlockIds; /// @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; + uint256[50] private __gap; // Reduced to account for new variables /* ─────────────────────────────── Structs ─────────────────────────────── */ @@ -104,6 +65,7 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, * @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 VestingV2. */ struct UnlockRequest { uint256 amount; @@ -112,6 +74,7 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, uint256 tokenId; bool cliffPassed; bool vestingStarted; + uint256 vestingPlanId; } /* ─────────────────────────────── Events ─────────────────────────────── */ @@ -145,12 +108,13 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, /** * @notice Emitted when vesting starts for an unlock request. - * @param unlockId ID of the unlock request. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - * @param amount Amount of tokens entering vesting. + * @param unlockId ID of the unlock request. + * @param vestingPlanId ID of the created vesting plan. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens entering vesting. */ - event VestingStarted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount); + event VestingStarted(uint256 indexed unlockId, uint256 indexed vestingPlanId, uint256 indexed tokenId, address owner, uint256 amount); /** * @notice Emitted when the cliff duration is updated. @@ -164,22 +128,14 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, */ event VestingDurationUpdated(uint256 newDuration); - /** - * @notice Emitted when the vesting contract address is updated. - * @param newVestingContract New vesting contract address. - */ - event VestingContractUpdated(address newVestingContract); - /* ─────────────────────────────── Errors ─────────────────────────────── */ - error NotNFTOwner(); // caller is not the owner of the NFT error UnlockNotFound(); // unlock request ID is invalid or not found error CliffNotPassed(); // cliff period has not yet passed error VestingAlreadyStarted(); // vesting has already started for this unlock request error InvalidDuration(); // invalid duration (zero) provided for cliff or vesting - error ZeroAddress(); // zero address provided for critical parameters - error ZeroAmount(); // zero amount provided for operations requiring non-zero value error UnauthorizedAccess(address caller, address requiredCaller); // unauthorized caller attempted restricted action + error ZeroAmount(); // zero amount provided for critical parameters /* ─────────────────────────── Initialization ─────────────────────────── */ @@ -190,12 +146,13 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, /** * @notice Initialize the SymmUnlockManager with core contracts and configuration. - * @param _symmBuildersNft Address of the SymmioBuildersNFT contract. - * @param _symm Address of the SYMM token contract. - * @param _vestingContract Address of the Vesting contract. - * @param _admin Address to receive admin and all role assignments. - * @param _cliffDuration Duration of the cliff period in seconds. - * @param _vestingDuration Duration of the vesting period in seconds. + * @param _symmBuildersNft Address of the SymmioBuildersNFT contract. + * @param _symm Address of the SYMM token contract. + * @param _admin Address to receive admin and all role assignments. + * @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. * * @dev Sets up access control and validates all inputs. Reverts on zero addresses or invalid durations. * This replaces the constructor for upgradeable contracts. @@ -203,56 +160,30 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, function initialize( address _symmBuildersNft, address _symm, - address _vestingContract, address _admin, uint256 _cliffDuration, - uint256 _vestingDuration + uint256 _vestingDuration, + uint256 _lockedClaimPenalty, + address _lockedClaimPenaltyReceiver ) public initializer { - if (_symmBuildersNft == address(0) || _symm == address(0) || _vestingContract == address(0) || _admin == address(0)) { + if (_symmBuildersNft == address(0) || _symm == address(0) || _admin == address(0)) { revert ZeroAddress(); } if (_cliffDuration == 0 || _vestingDuration == 0) { revert InvalidDuration(); } - // Initialize parent contracts - __AccessControlEnumerable_init(); - __Pausable_init(); - __ReentrancyGuard_init(); + // Initialize parent VestingV2 contract + __vesting_init(_admin, _lockedClaimPenalty, _lockedClaimPenaltyReceiver); // Set contract addresses and parameters symmBuildersNftManager = ISymmioBuildersNftManager(_symmBuildersNft); SYMM = IERC20(_symm); - vestingContract = IVesting(_vestingContract); cliffDuration = _cliffDuration; vestingDuration = _vestingDuration; // Initialize counter _unlockIdCounter = 0; - - // Grant roles to admin - _grantRole(DEFAULT_ADMIN_ROLE, _admin); - _grantRole(SETTER_ROLE, _admin); - _grantRole(PAUSER_ROLE, _admin); - _grantRole(UNPAUSER_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(); } /* ──────────────────── Unlock Management ──────────────────── */ @@ -281,7 +212,8 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, owner: owner, tokenId: tokenId, cliffPassed: false, - vestingStarted: false + vestingStarted: false, + vestingPlanId: 0 }); tokenUnlockIds[tokenId].push(unlockId); @@ -301,9 +233,6 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, if (request.amount == 0) { revert UnlockNotFound(); } - // if (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { - // revert NotNFTOwner(); - // } if (request.cliffPassed) { revert CliffNotPassed(); } @@ -335,7 +264,7 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, * @notice Complete the cliff period and start vesting for an unlock request. * @param unlockId ID of the unlock request to process. * - * @dev Transfers tokens to vesting contract and sets up vesting plan. + * @dev Uses inherited VestingV2 functionality to create a sophisticated vesting plan. * Only callable by NFT owner after cliff period completion. */ function completeCliffAndStartVesting(uint256 unlockId) external nonReentrant whenNotPaused { @@ -343,9 +272,6 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, if (request.amount == 0) { revert UnlockNotFound(); } - // if (symmBuildersNftManager.ownerOf(request.tokenId) != msg.sender) { - // revert NotNFTOwner(); - // } if (request.vestingStarted) { revert VestingAlreadyStarted(); } @@ -360,20 +286,19 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, // Complete unlock on NFT contract symmBuildersNftManager.completeUnlock(request.tokenId, request.amount); - // Set up vesting plan for the owner + // Create vesting plan using inherited VestingV2 functionality address[] memory users = new address[](1); users[0] = request.owner; uint256[] memory amounts = new uint256[](1); amounts[0] = request.amount; - // Approve vesting contract to transfer tokens - SYMM.approve(address(vestingContract), request.amount); + uint256[] memory planIds = _setupVestingPlans(address(SYMM), block.timestamp, block.timestamp + vestingDuration, users, amounts); - // Set up vesting plan starting from now - vestingContract.setupVestingPlans(address(SYMM), block.timestamp, block.timestamp + vestingDuration, users, amounts); + // Link vesting plan to unlock request + request.vestingPlanId = planIds[0]; emit CliffCompleted(unlockId, request.tokenId, request.owner); - emit VestingStarted(unlockId, request.tokenId, request.owner, request.amount); + emit VestingStarted(unlockId, planIds[0], request.tokenId, request.owner, request.amount); } /* ────────────────────────── Admin Functions ────────────────────────── */ @@ -406,32 +331,6 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, emit VestingDurationUpdated(_vestingDuration); } - /** - * @notice Update the vesting contract address. - * @param _vestingContract New vesting contract address. - * - * @dev Only callable by accounts with SETTER_ROLE. Cannot be zero address. - */ - function setVestingContract(address _vestingContract) external onlyRole(SETTER_ROLE) { - if (_vestingContract == address(0)) { - revert ZeroAddress(); - } - vestingContract = IVesting(_vestingContract); - emit VestingContractUpdated(_vestingContract); - } - - /** - * @notice Rescue tokens accidentally sent to the contract. - * @param token Address of the token to rescue. - * @param to Recipient address for the rescued tokens. - * @param amount Amount of tokens to transfer. - * - * @dev Only callable by accounts with DEFAULT_ADMIN_ROLE for emergency recovery. - */ - function rescueTokens(address token, address to, uint256 amount) external onlyRole(DEFAULT_ADMIN_ROLE) { - IERC20(token).safeTransfer(to, amount); - } - /* ────────────────────────── View Functions ────────────────────────── */ /** @@ -535,6 +434,30 @@ contract SymmUnlockManager is Initializable, AccessControlEnumerableUpgradeable, return cliffEndTime - block.timestamp; } + /** + * @notice Get the vesting plan ID for an unlock request. + * @param unlockId ID of the unlock request. + * @return vestingPlanId ID of the associated vesting plan (0 if not started). + */ + function getUnlockVestingPlanId(uint256 unlockId) external view returns (uint256 vestingPlanId) { + UnlockRequest storage request = unlockRequests[unlockId]; + return request.vestingPlanId; + } + + /** + * @notice Override to handle SYMM token minting if possible. + * @param token Address of the token to mint. + * @param amount Amount of tokens to mint. + * + * @dev This hook is called when the contract needs more tokens for vesting. + * In this case, we expect SYMM tokens to be transferred from the NFT manager. + */ + function _mintTokenIfPossible(address token, uint256 amount) internal virtual override { + // Since SYMM tokens come from burned NFTs, we don't mint them + // The tokens should already be in the contract from completed unlocks + // This is a no-op, but can be overridden if minting is needed + } + /** * @notice Returns the current version of the contract. * @return Version string of the contract. diff --git a/contracts/builders-nft/interfaces/ISymmBuildersNftUnlockManager.sol b/contracts/builders-nft/interfaces/ISymmioBuildersNftUnlockManager.sol similarity index 82% rename from contracts/builders-nft/interfaces/ISymmBuildersNftUnlockManager.sol rename to contracts/builders-nft/interfaces/ISymmioBuildersNftUnlockManager.sol index a3e46a0..31d8fbb 100644 --- a/contracts/builders-nft/interfaces/ISymmBuildersNftUnlockManager.sol +++ b/contracts/builders-nft/interfaces/ISymmioBuildersNftUnlockManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.27; -interface ISymmBuildersNftUnlockManager { +interface ISymmioBuildersNftUnlockManager { function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external; function isUnlocking(uint256 tokenId) external view returns (bool); From 53bc7df294456ca3d99d1d281a7e71c624e02a9f Mon Sep 17 00:00:00 2001 From: Naveed Date: Wed, 23 Jul 2025 14:56:15 +0200 Subject: [PATCH 35/48] Merge unlock manager into manager --- .../builders-nft/SymmioBuildersNftManager.sol | 452 +++++++++++++---- .../SymmioBuildersNftUnlockMangaer.sol | 469 ------------------ .../interfaces/ISymmioBuildersNftManager.sol | 8 - .../ISymmioBuildersNftUnlockManager.sol | 8 - 4 files changed, 356 insertions(+), 581 deletions(-) delete mode 100644 contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol delete mode 100644 contracts/builders-nft/interfaces/ISymmioBuildersNftManager.sol delete mode 100644 contracts/builders-nft/interfaces/ISymmioBuildersNftUnlockManager.sol diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 5297548..78811ee 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -4,30 +4,29 @@ pragma solidity ^0.8.27; /** * @title SymmioBuildersNftManager * @notice Comprehensive manager contract for SymmioBuildersNft that handles all complex logic - * including SYMM token locking, unlock processes, merging, fee collection, and cross-chain sync. + * including SYMM token locking, unlock processes with cliff and vesting, merging, + * fee collection, and cross-chain sync. Integrates full VestingV2 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 + * • Time-locked unlock functionality with cliff periods + * • Full VestingV2 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 "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlEnumerableUpgradeable.sol"; +import "../vesting/VestingV2.sol"; import "./interfaces/ISymmioBuildersNft.sol"; -import "./interfaces/ISymmioBuildersNftUnlockManager.sol"; -import "./interfaces/ISymmioBuildersNftManager.sol"; /* ────────────────────────── External Interfaces ────────────────────────── */ @@ -36,6 +35,11 @@ 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. */ @@ -43,45 +47,70 @@ interface ISymmFeeCollector { function onLockedAmountChanged(int256 amount) external; } -contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, ISymmioBuildersNftManager { +contract SymmioBuildersNftManager is VestingV2 { using SafeERC20 for IERC20; - /* ─────────────────────────────── Roles ─────────────────────────────── */ + /* ─────────────────────────────── Additional Roles ─────────────────────────────── */ /// @notice Role for minting NFTs without burning SYMM tokens. bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - /// @notice Role for updating configuration parameters. - bytes32 public constant SETTER_ROLE = keccak256("SETTER_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"); - /// @notice Role for syncing cross-chain lock data and minting NFTs. bytes32 public constant SYNC_ROLE = keccak256("SYNC_ROLE"); + /// @notice Role for updating cliff and vesting durations. + bytes32 public constant DURATION_SETTER_ROLE = keccak256("DURATION_SETTER_ROLE"); + /* ──────────────────────── Storage Variables ──────────────────────── */ - /// @notice The SYMM token contract address. + /// @notice The SYMM token contract address (burnable and mintable). IERC20Burnable public SYMM; /// @notice The SymmioBuildersNft contract. ISymmioBuildersNft public nftContract; - /// @notice The unlock manager contract for handling token unlock processes. - ISymmioBuildersNftUnlockManager public unlockManager; - /// @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; - /// @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; + /// @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 VestingV2. + */ + struct UnlockRequest { + uint256 amount; + uint256 unlockInitiatedTime; + address owner; + uint256 tokenId; + bool cliffPassed; + bool vestingStarted; + uint256 vestingPlanId; + } /* ─────────────────────────────── Events ─────────────────────────────── */ @@ -122,11 +151,40 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra /** * @notice Emitted when an unlock process is initiated for an NFT. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - * @param amount Amount of tokens to unlock. + * @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 UnlockInitiated(uint256 indexed tokenId, address indexed owner, uint256 amount); + event UnlockCancelled(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount); + + /** + * @notice Emitted when the cliff period for an unlock request is completed. + * @param unlockId ID of the unlock request. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + */ + event CliffCompleted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner); + + /** + * @notice Emitted when vesting starts for an unlock request. + * @param unlockId ID of the unlock request. + * @param vestingPlanId ID of the created vesting plan. + * @param tokenId ID of the NFT. + * @param owner Owner of the NFT. + * @param amount Amount of tokens entering vesting. + */ + event VestingStarted(uint256 indexed unlockId, uint256 indexed vestingPlanId, uint256 indexed tokenId, address owner, uint256 amount); /** * @notice Emitted when an unlock process is completed for an NFT. @@ -143,10 +201,16 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra event MinLockAmountUpdated(uint256 newMinAmount); /** - * @notice Emitted when the unlock manager address is updated. - * @param newUnlockManager New unlock manager address. + * @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 UnlockManagerUpdated(address newUnlockManager); + event VestingDurationUpdated(uint256 newDuration); /** * @notice Emitted when an NFT is minted for cross-chain synchronization. @@ -177,12 +241,14 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra error NotTokenOwner(); error InsufficientLockedAmount(); error InvalidTokenId(); - error ZeroAddress(); error ZeroAmount(); - error UnlockManagerNotSet(); error TokenHasActiveUnlock(); error UnauthorizedAccess(address caller, address requiredCaller); error LengthMismatch(); + error UnlockNotFound(); + error CliffNotPassed(); + error VestingAlreadyStarted(); + error InvalidDuration(); /* ─────────────────────────── Initialization ─────────────────────────── */ @@ -192,33 +258,48 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra } /** - * @notice Initialize the SymmioBuildersNftManager contract. - * @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. + * @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) public initializer { + 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)) revert ZeroAddress(); if (_minLockAmount == 0) revert ZeroAmount(); + if (_cliffDuration == 0 || _vestingDuration == 0) revert InvalidDuration(); + if (_lockedClaimPenaltyReceiver == address(0)) revert ZeroAddress(); - // Initialize parent contracts - __AccessControlEnumerable_init(); - __Pausable_init(); - __ReentrancyGuard_init(); + // Initialize parent VestingV2 contract + __vesting_init(_admin, _lockedClaimPenalty, _lockedClaimPenaltyReceiver); // Set contract-specific state SYMM = IERC20Burnable(_symm); nftContract = ISymmioBuildersNft(_nftContract); minLockAmount = _minLockAmount; + cliffDuration = _cliffDuration; + vestingDuration = _vestingDuration; - // Grant all roles to the admin for initial setup - _grantRole(DEFAULT_ADMIN_ROLE, _admin); + // Initialize counter + _unlockIdCounter = 0; + + // Grant additional roles to the admin for initial setup _grantRole(MINTER_ROLE, _admin); - _grantRole(SETTER_ROLE, _admin); - _grantRole(PAUSER_ROLE, _admin); - _grantRole(UNPAUSER_ROLE, _admin); _grantRole(SYNC_ROLE, _admin); + _grantRole(DURATION_SETTER_ROLE, _admin); } /* ────────────────────── Core NFT & Locking Functions ────────────────────── */ @@ -329,9 +410,8 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra * @param tokenId ID of the NFT to unlock from. * @param amount Amount of tokens to unlock. */ - function initiateUnlock(uint256 tokenId, uint256 amount) external nonReentrant { + function initiateUnlock(uint256 tokenId, uint256 amount) external nonReentrant whenNotPaused { if (nftContract.ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); - if (address(unlockManager) == address(0)) revert UnlockManagerNotSet(); ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); uint256 availableAmount = data.amount - data.unlockingAmount; @@ -342,49 +422,107 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra // Update the unlocking amount nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount + amount, data.name); - // Delegate to the unlock manager - unlockManager.initiateUnlock(tokenId, msg.sender, amount); + // Create unlock request + uint256 unlockId = _unlockIdCounter++; + unlockRequests[unlockId] = UnlockRequest({ + amount: amount, + unlockInitiatedTime: block.timestamp, + owner: msg.sender, + tokenId: tokenId, + cliffPassed: false, + vestingStarted: false, + vestingPlanId: 0 + }); + + tokenUnlockIds[tokenId].push(unlockId); // Notify fee collectors _notifyFeeCollectors(tokenId, -int256(amount)); - emit UnlockInitiated(tokenId, msg.sender, amount); + emit UnlockInitiated(unlockId, tokenId, msg.sender, amount, block.timestamp + cliffDuration); } /** - * @notice Complete the unlock process for an NFT. - * @param tokenId ID of the NFT to unlock. - * @param amount Amount of tokens being unlocked. + * @notice Cancel an unlock request before the cliff period ends. + * @param unlockId ID of the unlock request to cancel. * - * @dev Only callable by the unlock manager. Burns NFT if no tokens remain. + * @dev Removes the unlock request and updates NFT contract. + * Only callable by the NFT owner and only before cliff completion. */ - function completeUnlock(uint256 tokenId, uint256 amount) external { - if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); + 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.cliffPassed) revert CliffNotPassed(); + + 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 - amount, data.unlockingAmount - amount, data.name); + nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount - amount, data.name); - // Burn the NFT if no locked tokens remain - if (data.amount == 0) nftContract.burn(tokenId); + // Notify fee collectors + _notifyFeeCollectors(tokenId, int256(amount)); - emit UnlockCompleted(tokenId, msg.sender, amount); + emit UnlockCancelled(unlockId, tokenId, owner, amount); } /** - * @notice Cancel an unlock process for an NFT. - * @param tokenId ID of the NFT to cancel the unlock for. - * @param amount Amount to cancel from the unlocking process. + * @notice Complete the cliff period and start vesting for an unlock request. + * @param unlockId ID of the unlock request to process. * - * @dev Only callable by the unlock manager. + * @dev Uses inherited VestingV2 functionality to create a sophisticated vesting plan. + * Only callable by NFT owner after cliff period completion. */ - function cancelUnlock(uint256 tokenId, uint256 amount) external { - if (msg.sender != address(unlockManager)) revert UnauthorizedAccess(msg.sender, address(unlockManager)); + 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(); - ISymmioBuildersNft.LockData memory data = nftContract.getLockData(tokenId); - nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount - amount, data.name); + // Mark cliff as passed and vesting as started + request.cliffPassed = true; + request.vestingStarted = true; - // Notify fee collectors - _notifyFeeCollectors(tokenId, int256(amount)); + // Complete unlock on NFT contract + ISymmioBuildersNft.LockData memory data = nftContract.getLockData(request.tokenId); + nftContract.updateLockData(request.tokenId, data.amount - request.amount, data.unlockingAmount - request.amount, data.name); + + // Burn the NFT if no locked tokens remain + if (data.amount - request.amount == 0) { + nftContract.burn(request.tokenId); + } + + // Create vesting plan using inherited VestingV2 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 CliffCompleted(unlockId, request.tokenId, request.owner); + emit VestingStarted(unlockId, planIds[0], request.tokenId, request.owner, request.amount); + emit UnlockCompleted(request.tokenId, request.owner, request.amount); } /* ───────────────────── Cross-Chain Sync Functions ───────────────────── */ @@ -424,22 +562,6 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra } } - /* ───────────────────────── Pause Controls ───────────────────────── */ - - /** - * @notice Pause the contract, disabling state-changing functions. - */ - function pause() external onlyRole(PAUSER_ROLE) { - _pause(); - } - - /** - * @notice Unpause the contract, enabling state-changing functions. - */ - function unpause() external onlyRole(UNPAUSER_ROLE) { - _unpause(); - } - /* ────────────────────────── Admin Functions ────────────────────────── */ /** @@ -453,13 +575,27 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra } /** - * @notice Set the address of the unlock manager contract. - * @param _unlockManager New unlock manager address. + * @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(DURATION_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 setUnlockManager(address _unlockManager) external onlyRole(SETTER_ROLE) { - if (_unlockManager == address(0)) revert ZeroAddress(); - unlockManager = ISymmioBuildersNftUnlockManager(_unlockManager); - emit UnlockManagerUpdated(_unlockManager); + function setVestingDuration(uint256 _vestingDuration) external onlyRole(DURATION_SETTER_ROLE) { + if (_vestingDuration == 0) revert InvalidDuration(); + vestingDuration = _vestingDuration; + emit VestingDurationUpdated(_vestingDuration); } /** @@ -502,6 +638,117 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra 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 active unlock requests for a specific NFT. + * @param tokenId ID of the NFT to query. + * @return Array of active UnlockRequest structs (excluding completed/vesting requests). + * + * @dev Filters out requests that have started vesting or been completed. + */ + function getActiveUnlockRequests(uint256 tokenId) external view returns (UnlockRequest[] memory) { + uint256[] memory unlockIds = tokenUnlockIds[tokenId]; + uint256 activeCount = 0; + + // Count active requests (non-zero amount and not vesting) + for (uint256 i = 0; i < unlockIds.length; i++) { + if (unlockRequests[unlockIds[i]].amount > 0 && !unlockRequests[unlockIds[i]].vestingStarted) { + activeCount++; + } + } + + // Populate active requests array + UnlockRequest[] memory activeRequests = new UnlockRequest[](activeCount); + uint256 index = 0; + for (uint256 i = 0; i < unlockIds.length; i++) { + UnlockRequest storage request = unlockRequests[unlockIds[i]]; + if (request.amount > 0 && !request.vestingStarted) { + activeRequests[index++] = request; + } + } + + return activeRequests; + } + + /** + * @notice Check if an NFT has any active unlock requests. + * @param tokenId ID of the NFT to check. + * @return Whether the NFT has active unlock requests. + */ + function isUnlocking(uint256 tokenId) external view returns (bool) { + uint256[] memory unlockIds = tokenUnlockIds[tokenId]; + for (uint256 i = 0; i < unlockIds.length; i++) { + UnlockRequest storage request = unlockRequests[unlockIds[i]]; + if (request.amount > 0 && !request.vestingStarted) { + return true; + } + } + return false; + } + + /** + * @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) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return 0; + } + return request.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) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return false; + } + return block.timestamp >= request.unlockInitiatedTime + cliffDuration; + } + + /** + * @notice Get the time remaining in the cliff period for an unlock request. + * @param unlockId ID of the unlock request. + * @return Seconds remaining until cliff period ends, or 0 if passed/invalid. + */ + function getCliffTimeRemaining(uint256 unlockId) external view returns (uint256) { + UnlockRequest storage request = unlockRequests[unlockId]; + if (request.amount == 0) { + return 0; + } + + uint256 cliffEndTime = request.unlockInitiatedTime + cliffDuration; + if (block.timestamp >= cliffEndTime) { + return 0; + } + + return cliffEndTime - block.timestamp; + } + + /** + * @notice Get the vesting plan ID for an unlock request. + * @param unlockId ID of the unlock request. + * @return vestingPlanId ID of the associated vesting plan (0 if not started). + */ + function getUnlockVestingPlanId(uint256 unlockId) external view returns (uint256 vestingPlanId) { + UnlockRequest storage request = unlockRequests[unlockId]; + return request.vestingPlanId; + } + /* ───────────────────────── Internal Helpers ───────────────────────── */ /** @@ -516,6 +763,19 @@ contract SymmioBuildersNftManager is Initializable, AccessControlEnumerableUpgra } } + /** + * @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. diff --git a/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol b/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol deleted file mode 100644 index 248f427..0000000 --- a/contracts/builders-nft/SymmioBuildersNftUnlockMangaer.sol +++ /dev/null @@ -1,469 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -/** - * @title SymmUnlockManager - * @notice Manages the unlock process for SYMM tokens locked in SymmioBuildersNFTManager contracts with - * integrated cliff periods and sophisticated vesting functionality. Inherits from VestingV2 to - * provide comprehensive token vesting with penalty mechanisms and flexible claiming options. - * - * @dev Core features include: - * • Unlock request management with unique ID tracking - * • Configurable cliff period enforcement before token release - * • Full VestingV2 functionality inherited (linear vesting, penalties, percentage claims) - * • Cancellation functionality for unlock requests during cliff period - * • Comprehensive tracking of unlock status and timing - * • Emergency pause functionality for security incidents - * • Token rescue capabilities for administrative recovery - * • Detailed view functions for unlock request analysis - * - * The contract coordinates with SymmioBuildersNFTManager for lock management and uses - * inherited VestingV2 functionality for sophisticated token distribution with penalties, - * percentage-based claiming, and multiple vesting plans per user. - * - * @dev This contract is designed to be used with OpenZeppelin's TransparentUpgradeableProxy. - */ - -import "../vesting/VestingV2.sol"; -import "./interfaces/ISymmioBuildersNft.sol"; -import "./interfaces/ISymmioBuildersNftManager.sol"; - -contract SymmioBuildersNftUnlockManager is VestingV2 { - /* ──────────────────────── Additional Storage Variables ──────────────────────── */ - - /// @notice The SymmioBuildersNFT contract. - ISymmioBuildersNftManager public symmBuildersNftManager; - - /// @notice The SYMM token contract. - IERC20 public SYMM; - - /// @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 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; - - /// @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; // Reduced to account for new variables - - /* ─────────────────────────────── 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 VestingV2. - */ - struct UnlockRequest { - uint256 amount; - uint256 unlockInitiatedTime; - address owner; - uint256 tokenId; - bool cliffPassed; - bool vestingStarted; - uint256 vestingPlanId; - } - - /* ─────────────────────────────── Events ─────────────────────────────── */ - - /** - * @notice Emitted when an unlock request is initiated. - * @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 request 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 the cliff period for an unlock request is completed. - * @param unlockId ID of the unlock request. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - */ - event CliffCompleted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner); - - /** - * @notice Emitted when vesting starts for an unlock request. - * @param unlockId ID of the unlock request. - * @param vestingPlanId ID of the created vesting plan. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - * @param amount Amount of tokens entering vesting. - */ - event VestingStarted(uint256 indexed unlockId, uint256 indexed vestingPlanId, uint256 indexed tokenId, address owner, uint256 amount); - - /** - * @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); - - /* ─────────────────────────────── Errors ─────────────────────────────── */ - - error UnlockNotFound(); // unlock request ID is invalid or not found - error CliffNotPassed(); // cliff period has not yet passed - error VestingAlreadyStarted(); // vesting has already started for this unlock request - error InvalidDuration(); // invalid duration (zero) provided for cliff or vesting - error UnauthorizedAccess(address caller, address requiredCaller); // unauthorized caller attempted restricted action - error ZeroAmount(); // zero amount provided for critical parameters - - /* ─────────────────────────── Initialization ─────────────────────────── */ - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - /** - * @notice Initialize the SymmUnlockManager with core contracts and configuration. - * @param _symmBuildersNft Address of the SymmioBuildersNFT contract. - * @param _symm Address of the SYMM token contract. - * @param _admin Address to receive admin and all role assignments. - * @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. - * - * @dev Sets up access control and validates all inputs. Reverts on zero addresses or invalid durations. - * This replaces the constructor for upgradeable contracts. - */ - function initialize( - address _symmBuildersNft, - address _symm, - address _admin, - uint256 _cliffDuration, - uint256 _vestingDuration, - uint256 _lockedClaimPenalty, - address _lockedClaimPenaltyReceiver - ) public initializer { - if (_symmBuildersNft == address(0) || _symm == address(0) || _admin == address(0)) { - revert ZeroAddress(); - } - if (_cliffDuration == 0 || _vestingDuration == 0) { - revert InvalidDuration(); - } - - // Initialize parent VestingV2 contract - __vesting_init(_admin, _lockedClaimPenalty, _lockedClaimPenaltyReceiver); - - // Set contract addresses and parameters - symmBuildersNftManager = ISymmioBuildersNftManager(_symmBuildersNft); - SYMM = IERC20(_symm); - cliffDuration = _cliffDuration; - vestingDuration = _vestingDuration; - - // Initialize counter - _unlockIdCounter = 0; - } - - /* ──────────────────── Unlock Management ──────────────────── */ - - /** - * @notice Initiate an unlock request for an NFT (called by SymmioBuildersNFT contract). - * @param tokenId ID of the NFT to unlock. - * @param owner Owner of the NFT. - * @param amount Amount of tokens to unlock. - * - * @dev Creates a new unlock request with cliff period enforcement. - * Only callable by the SymmioBuildersNFT contract. - */ - function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external whenNotPaused { - if (msg.sender != address(symmBuildersNftManager)) { - revert UnauthorizedAccess(msg.sender, address(symmBuildersNftManager)); - } - if (amount == 0) { - revert ZeroAmount(); - } - - uint256 unlockId = _unlockIdCounter++; - unlockRequests[unlockId] = UnlockRequest({ - amount: amount, - unlockInitiatedTime: block.timestamp, - owner: owner, - tokenId: tokenId, - cliffPassed: false, - vestingStarted: false, - vestingPlanId: 0 - }); - - tokenUnlockIds[tokenId].push(unlockId); - - emit UnlockInitiated(unlockId, tokenId, owner, 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 notifies the 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.cliffPassed) { - revert CliffNotPassed(); - } - - 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; - } - } - - // Notify NFT contract to cancel the unlock - symmBuildersNftManager.cancelUnlock(tokenId, 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 VestingV2 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.vestingStarted) { - revert VestingAlreadyStarted(); - } - if (block.timestamp < request.unlockInitiatedTime + cliffDuration) { - revert CliffNotPassed(); - } - - // Mark cliff as passed and vesting as started - request.cliffPassed = true; - request.vestingStarted = true; - - // Complete unlock on NFT contract - symmBuildersNftManager.completeUnlock(request.tokenId, request.amount); - - // Create vesting plan using inherited VestingV2 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 CliffCompleted(unlockId, request.tokenId, request.owner); - emit VestingStarted(unlockId, planIds[0], request.tokenId, request.owner, request.amount); - } - - /* ────────────────────────── Admin Functions ────────────────────────── */ - - /** - * @notice Update the cliff duration for new unlock requests. - * @param _cliffDuration New cliff duration in seconds. - * - * @dev Only callable by accounts with 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 SETTER_ROLE. Must be non-zero. - */ - function setVestingDuration(uint256 _vestingDuration) external onlyRole(SETTER_ROLE) { - if (_vestingDuration == 0) { - revert InvalidDuration(); - } - vestingDuration = _vestingDuration; - emit VestingDurationUpdated(_vestingDuration); - } - - /* ────────────────────────── View Functions ────────────────────────── */ - - /** - * @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 active unlock requests for a specific NFT. - * @param tokenId ID of the NFT to query. - * @return Array of active UnlockRequest structs (excluding completed/vesting requests). - * - * @dev Filters out requests that have started vesting or been completed. - */ - function getActiveUnlockRequests(uint256 tokenId) external view returns (UnlockRequest[] memory) { - uint256[] memory unlockIds = tokenUnlockIds[tokenId]; - uint256 activeCount = 0; - - // Count active requests (non-zero amount and not vesting) - for (uint256 i = 0; i < unlockIds.length; i++) { - if (unlockRequests[unlockIds[i]].amount > 0 && !unlockRequests[unlockIds[i]].vestingStarted) { - activeCount++; - } - } - - // Populate active requests array - UnlockRequest[] memory activeRequests = new UnlockRequest[](activeCount); - uint256 index = 0; - for (uint256 i = 0; i < unlockIds.length; i++) { - UnlockRequest storage request = unlockRequests[unlockIds[i]]; - if (request.amount > 0 && !request.vestingStarted) { - activeRequests[index++] = request; - } - } - - return activeRequests; - } - - /** - * @notice Check if an NFT has any active unlock requests. - * @param tokenId ID of the NFT to check. - * @return Whether the NFT has active unlock requests. - */ - function isUnlocking(uint256 tokenId) external view returns (bool) { - uint256[] memory unlockIds = tokenUnlockIds[tokenId]; - for (uint256 i = 0; i < unlockIds.length; i++) { - UnlockRequest storage request = unlockRequests[unlockIds[i]]; - if (request.amount > 0 && !request.vestingStarted) { - return true; - } - } - return false; - } - - /** - * @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) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return 0; - } - return request.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) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return false; - } - return block.timestamp >= request.unlockInitiatedTime + cliffDuration; - } - - /** - * @notice Get the time remaining in the cliff period for an unlock request. - * @param unlockId ID of the unlock request. - * @return Seconds remaining until cliff period ends, or 0 if passed/invalid. - */ - function getCliffTimeRemaining(uint256 unlockId) external view returns (uint256) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return 0; - } - - uint256 cliffEndTime = request.unlockInitiatedTime + cliffDuration; - if (block.timestamp >= cliffEndTime) { - return 0; - } - - return cliffEndTime - block.timestamp; - } - - /** - * @notice Get the vesting plan ID for an unlock request. - * @param unlockId ID of the unlock request. - * @return vestingPlanId ID of the associated vesting plan (0 if not started). - */ - function getUnlockVestingPlanId(uint256 unlockId) external view returns (uint256 vestingPlanId) { - UnlockRequest storage request = unlockRequests[unlockId]; - return request.vestingPlanId; - } - - /** - * @notice Override to handle SYMM token minting if possible. - * @param token Address of the token to mint. - * @param amount Amount of tokens to mint. - * - * @dev This hook is called when the contract needs more tokens for vesting. - * In this case, we expect SYMM tokens to be transferred from the NFT manager. - */ - function _mintTokenIfPossible(address token, uint256 amount) internal virtual override { - // Since SYMM tokens come from burned NFTs, we don't mint them - // The tokens should already be in the contract from completed unlocks - // This is a no-op, but can be overridden if minting is needed - } - - /** - * @notice Returns the current version of the contract. - * @return Version string of the contract. - * @dev This function can be used to verify which version of the contract is deployed. - */ - function version() external pure returns (string memory) { - return "1.0.0"; - } -} diff --git a/contracts/builders-nft/interfaces/ISymmioBuildersNftManager.sol b/contracts/builders-nft/interfaces/ISymmioBuildersNftManager.sol deleted file mode 100644 index fc6c6ae..0000000 --- a/contracts/builders-nft/interfaces/ISymmioBuildersNftManager.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -interface ISymmioBuildersNftManager { - function completeUnlock(uint256 tokenId, uint256 amount) external; - - function cancelUnlock(uint256 tokenId, uint256 amount) external; -} diff --git a/contracts/builders-nft/interfaces/ISymmioBuildersNftUnlockManager.sol b/contracts/builders-nft/interfaces/ISymmioBuildersNftUnlockManager.sol deleted file mode 100644 index 31d8fbb..0000000 --- a/contracts/builders-nft/interfaces/ISymmioBuildersNftUnlockManager.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -interface ISymmioBuildersNftUnlockManager { - function initiateUnlock(uint256 tokenId, address owner, uint256 amount) external; - - function isUnlocking(uint256 tokenId) external view returns (bool); -} From aa2a87d7e99efe14730ddd31a7bf173cfd2700d7 Mon Sep 17 00:00:00 2001 From: Naveed Date: Thu, 24 Jul 2025 07:47:33 +0200 Subject: [PATCH 36/48] Apply minor enhancement --- contracts/builders-nft/SymmioBuildersNft.sol | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index 9f6c012..b03f69a 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -38,9 +38,6 @@ contract SymmioBuildersNft is /// @notice Role for unpausing the contract operations. bytes32 public constant UNPAUSER_ROLE = keccak256("UNPAUSER_ROLE"); - /// @notice Role for pausing/unpausing NFT transfers specifically. - bytes32 public constant TRANSFER_PAUSER_ROLE = keccak256("TRANSFER_PAUSER_ROLE"); - /* ──────────────────────── Storage Variables ──────────────────────── */ /// @notice Counter for generating unique token IDs sequentially. @@ -254,14 +251,21 @@ contract SymmioBuildersNft is } /** - * @notice Set the pause state for NFT transfers independently of contract pause. - * @param _paused True to pause transfers, false to unpause. - * - * @dev Only callable by accounts with TRANSFER_PAUSER_ROLE. + * @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 setTransfersPaused(bool _paused) external onlyRole(TRANSFER_PAUSER_ROLE) { - transfersPaused = _paused; - emit TransfersPausedUpdated(_paused); + function unpauseTransfers() external onlyRole(UNPAUSER_ROLE) { + transfersPaused = false; + emit TransfersPausedUpdated(false); } /* ───────────────────────── Internal Overrides ───────────────────────── */ From 03711be3d56c12261ed2ab152075e30eaf6dc216 Mon Sep 17 00:00:00 2001 From: Naveed Date: Thu, 24 Jul 2025 07:55:56 +0200 Subject: [PATCH 37/48] Fix minor problems --- contracts/builders-nft/SymmioBuildersNft.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index b03f69a..1b2f771 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -83,7 +83,6 @@ contract SymmioBuildersNft is 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 ─────────────────────────── */ @@ -171,11 +170,9 @@ contract SymmioBuildersNft is * @dev Only callable by the NFT owner. */ function updateLockData(uint256 tokenId, uint256 amount, uint256 unlockingAmount, string memory name) external onlyRole(MINTER_ROLE) { - if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); - lockData[tokenId] = ISymmioBuildersNft.LockData({ amount: amount, - lockTimestamp: block.timestamp, + lockTimestamp: lockData[tokenId].lockTimestamp, unlockingAmount: unlockingAmount, name: name }); @@ -287,7 +284,6 @@ contract SymmioBuildersNft is // 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); From 7079c49237b91d4db832de4363d31e77fc522389 Mon Sep 17 00:00:00 2001 From: Naveed Date: Thu, 24 Jul 2025 12:45:34 +0200 Subject: [PATCH 38/48] Fix minor problems --- contracts/builders-nft/SymmioBuildersNftManager.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 78811ee..3c3011c 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -241,6 +241,7 @@ contract SymmioBuildersNftManager is VestingV2 { error NotTokenOwner(); error InsufficientLockedAmount(); error InvalidTokenId(); + error InvalidMerge(); error ZeroAmount(); error TokenHasActiveUnlock(); error UnauthorizedAccess(address caller, address requiredCaller); @@ -293,9 +294,6 @@ contract SymmioBuildersNftManager is VestingV2 { cliffDuration = _cliffDuration; vestingDuration = _vestingDuration; - // Initialize counter - _unlockIdCounter = 0; - // Grant additional roles to the admin for initial setup _grantRole(MINTER_ROLE, _admin); _grantRole(SYNC_ROLE, _admin); @@ -342,6 +340,7 @@ contract SymmioBuildersNftManager is VestingV2 { 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); @@ -383,6 +382,7 @@ contract SymmioBuildersNftManager is VestingV2 { function merge(uint256 targetTokenId, uint256 sourceTokenId) external nonReentrant whenNotPaused { if (nftContract.ownerOf(targetTokenId) != msg.sender) revert NotTokenOwner(); if (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); @@ -505,7 +505,7 @@ contract SymmioBuildersNftManager is VestingV2 { nftContract.updateLockData(request.tokenId, data.amount - request.amount, data.unlockingAmount - request.amount, data.name); // Burn the NFT if no locked tokens remain - if (data.amount - request.amount == 0) { + if (data.amount - request.amount == 0 && data.unlockingAmount == 0) { nftContract.burn(request.tokenId); } @@ -535,6 +535,8 @@ contract SymmioBuildersNftManager is VestingV2 { * @param name Brand name for the NFT. */ function syncMint(address to, uint256 tokenId, uint256 amount, string memory name) external onlyRole(SYNC_ROLE) whenNotPaused { + if (to == address(0)) revert ZeroAddress(); + // Mint NFT with specific ID nftContract.mintWithId(to, tokenId, amount, name); From 9063f8b4db3946fc356606f63f8ff6990fb7bf5e Mon Sep 17 00:00:00 2001 From: Naveed Date: Thu, 24 Jul 2025 13:01:22 +0200 Subject: [PATCH 39/48] Apply minor enhancement --- contracts/builders-nft/SymmioBuildersNftManager.sol | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 3c3011c..6712668 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -58,9 +58,6 @@ contract SymmioBuildersNftManager is VestingV2 { /// @notice Role for syncing cross-chain lock data and minting NFTs. bytes32 public constant SYNC_ROLE = keccak256("SYNC_ROLE"); - /// @notice Role for updating cliff and vesting durations. - bytes32 public constant DURATION_SETTER_ROLE = keccak256("DURATION_SETTER_ROLE"); - /* ──────────────────────── Storage Variables ──────────────────────── */ /// @notice The SYMM token contract address (burnable and mintable). @@ -284,7 +281,7 @@ contract SymmioBuildersNftManager is VestingV2 { if (_cliffDuration == 0 || _vestingDuration == 0) revert InvalidDuration(); if (_lockedClaimPenaltyReceiver == address(0)) revert ZeroAddress(); - // Initialize parent VestingV2 contract + // Initialize parent Vesting contract __vesting_init(_admin, _lockedClaimPenalty, _lockedClaimPenaltyReceiver); // Set contract-specific state @@ -296,8 +293,7 @@ contract SymmioBuildersNftManager is VestingV2 { // Grant additional roles to the admin for initial setup _grantRole(MINTER_ROLE, _admin); - _grantRole(SYNC_ROLE, _admin); - _grantRole(DURATION_SETTER_ROLE, _admin); + _grantRole(SETTER_ROLE, _admin); } /* ────────────────────── Core NFT & Locking Functions ────────────────────── */ @@ -358,6 +354,7 @@ contract SymmioBuildersNftManager is VestingV2 { */ function lock(uint256 tokenId, uint256 amount) external nonReentrant whenNotPaused { if (nftContract.ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); + if (amount == 0) revert ZeroAmount(); // Burn the SYMM tokens SYMM.burnFrom(msg.sender, amount); @@ -582,7 +579,7 @@ contract SymmioBuildersNftManager is VestingV2 { * * @dev Only callable by accounts with DURATION_SETTER_ROLE. Must be non-zero. */ - function setCliffDuration(uint256 _cliffDuration) external onlyRole(DURATION_SETTER_ROLE) { + function setCliffDuration(uint256 _cliffDuration) external onlyRole(SETTER_ROLE) { if (_cliffDuration == 0) revert InvalidDuration(); cliffDuration = _cliffDuration; emit CliffDurationUpdated(_cliffDuration); @@ -594,7 +591,7 @@ contract SymmioBuildersNftManager is VestingV2 { * * @dev Only callable by accounts with DURATION_SETTER_ROLE. Must be non-zero. */ - function setVestingDuration(uint256 _vestingDuration) external onlyRole(DURATION_SETTER_ROLE) { + function setVestingDuration(uint256 _vestingDuration) external onlyRole(SETTER_ROLE) { if (_vestingDuration == 0) revert InvalidDuration(); vestingDuration = _vestingDuration; emit VestingDurationUpdated(_vestingDuration); From d511795cfd80030febcae0e33f2b5bc3cbd9bd1e Mon Sep 17 00:00:00 2001 From: Naveed Date: Thu, 24 Jul 2025 16:02:49 +0200 Subject: [PATCH 40/48] Fix minor problem --- contracts/builders-nft/SymmioBuildersNft.sol | 19 ------------- .../builders-nft/SymmioBuildersNftManager.sol | 28 ------------------- .../interfaces/ISymmioBuildersNft.sol | 2 -- hardhat.config.ts | 12 ++++---- 4 files changed, 6 insertions(+), 55 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index 1b2f771..341e21e 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -131,25 +131,6 @@ contract SymmioBuildersNft is emit NFTMinted(to, tokenId, amount, name); } - /** - * @notice Mint a specific NFT ID with a brand name (used for cross-chain sync). - * @param to Address to mint the NFT to. - * @param tokenId Specific token ID to mint. - * @param amount Amount of SYMM tokens to lock. - * @param name Name for the NFT. - * - * @dev Only callable by addresses with MINTER_ROLE. Updates counter to avoid conflicts. - */ - function mintWithId(address to, uint256 tokenId, uint256 amount, string memory name) external onlyRole(MINTER_ROLE) whenNotPaused { - // Update token ID counter to avoid conflicts - if (tokenId >= _tokenIdCounter) _tokenIdCounter = tokenId + 1; - - _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. diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 6712668..35bdf25 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -209,15 +209,6 @@ contract SymmioBuildersNftManager is VestingV2 { */ event VestingDurationUpdated(uint256 newDuration); - /** - * @notice Emitted when an NFT is minted for cross-chain synchronization. - * @param to Address receiving the NFT. - * @param tokenId ID of the minted NFT. - * @param amount Amount of SYMM tokens locked. - * @param brandName Brand name associated with the NFT. - */ - event SyncMint(address indexed to, uint256 indexed tokenId, uint256 amount, string brandName); - /** * @notice Emitted when fee collectors are added to an NFT. * @param tokenId ID of the NFT. @@ -524,25 +515,6 @@ contract SymmioBuildersNftManager is VestingV2 { /* ───────────────────── Cross-Chain Sync Functions ───────────────────── */ - /** - * @notice Mint an NFT without token transfer for cross-chain synchronization. - * @param to Address to mint the NFT to. - * @param tokenId Specific token ID to mint. - * @param amount Amount of SYMM tokens locked. - * @param name Brand name for the NFT. - */ - function syncMint(address to, uint256 tokenId, uint256 amount, string memory name) external onlyRole(SYNC_ROLE) whenNotPaused { - if (to == address(0)) revert ZeroAddress(); - - // Mint NFT with specific ID - nftContract.mintWithId(to, tokenId, amount, name); - - // Notify fee collectors - _notifyFeeCollectors(tokenId, int256(amount)); - - emit SyncMint(to, tokenId, amount, name); - } - /** * @notice Update lock data for multiple NFTs for cross-chain synchronization. * @param tokenIds Array of token IDs to update. diff --git a/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol b/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol index c060f13..fcf342b 100644 --- a/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol +++ b/contracts/builders-nft/interfaces/ISymmioBuildersNft.sol @@ -19,8 +19,6 @@ interface ISymmioBuildersNft is IERC721 { function mint(address to, uint256 amount, string memory name) external returns (uint256 tokenId); - function mintWithId(address to, uint256 tokenId, uint256 amount, string memory name) external; - function burn(uint256 tokenId) external; function getLockData(uint256 tokenId) external view returns (LockData memory); diff --git a/hardhat.config.ts b/hardhat.config.ts index f9be756..f513234 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -10,7 +10,7 @@ import "solidity-coverage" 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", @@ -43,17 +43,17 @@ export const config: HardhatUserConfig = { networks: { hardhat: { - forking: { - url: "https://1rpc.io/base", - blockNumber: 33113717, - }, + // 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: { From bb4fd215f274e95c49b3ffb24c106fd5d6ea32f7 Mon Sep 17 00:00:00 2001 From: Naveed Date: Sat, 26 Jul 2025 10:19:54 +0200 Subject: [PATCH 41/48] Fix minor problems --- .../builders-nft/SymmioBuildersNftManager.sol | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 35bdf25..dfb774a 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -5,14 +5,14 @@ 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 VestingV2 functionality. + * 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 VestingV2 functionality (linear vesting, penalties, percentage claims) + * • Full Vesting functionality (linear vesting, penalties, percentage claims) * • Unlock request management with unique ID tracking * • Fee collector management and notifications * • Cross-chain synchronization capabilities @@ -43,7 +43,7 @@ interface IERC20Mintable is IERC20 { /** * @notice Interface for the fee collector contract handling fee collection. */ -interface ISymmFeeCollector { +interface ISymmioFeeCollector { function onLockedAmountChanged(int256 amount) external; } @@ -97,7 +97,7 @@ contract SymmioBuildersNftManager is VestingV2 { * @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 VestingV2. + * @param vestingPlanId ID of the created vesting plan in Vesting. */ struct UnlockRequest { uint256 amount; @@ -267,10 +267,9 @@ contract SymmioBuildersNftManager is VestingV2 { uint256 _lockedClaimPenalty, address _lockedClaimPenaltyReceiver ) public initializer { - if (_symm == address(0) || _nftContract == address(0) || _admin == address(0)) revert ZeroAddress(); - if (_minLockAmount == 0) revert ZeroAmount(); - if (_cliffDuration == 0 || _vestingDuration == 0) revert InvalidDuration(); - if (_lockedClaimPenaltyReceiver == address(0)) revert ZeroAddress(); + 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); @@ -474,7 +473,7 @@ contract SymmioBuildersNftManager is VestingV2 { * @notice Complete the cliff period and start vesting for an unlock request. * @param unlockId ID of the unlock request to process. * - * @dev Uses inherited VestingV2 functionality to create a sophisticated vesting plan. + * @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 { @@ -497,7 +496,7 @@ contract SymmioBuildersNftManager is VestingV2 { nftContract.burn(request.tokenId); } - // Create vesting plan using inherited VestingV2 functionality + // Create vesting plan using inherited Vesting functionality address[] memory users = new address[](1); users[0] = request.owner; uint256[] memory amounts = new uint256[](1); @@ -730,7 +729,7 @@ contract SymmioBuildersNftManager is VestingV2 { function _notifyFeeCollectors(uint256 tokenId, int256 amount) private { address[] storage collectors = tokenRelatedFeeCollectors[tokenId]; for (uint256 i = 0; i < collectors.length; i++) { - ISymmFeeCollector(collectors[i]).onLockedAmountChanged(amount); + ISymmioFeeCollector(collectors[i]).onLockedAmountChanged(amount); } } From 5f565777f46fec1e189750ba0dce462d11d87f61 Mon Sep 17 00:00:00 2001 From: Naveed Date: Sat, 26 Jul 2025 10:35:59 +0200 Subject: [PATCH 42/48] Fix minor problems --- .../builders-nft/SymmioBuildersNftManager.sol | 43 +++---------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index dfb774a..83afad9 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -104,7 +104,6 @@ contract SymmioBuildersNftManager is VestingV2 { uint256 unlockInitiatedTime; address owner; uint256 tokenId; - bool cliffPassed; bool vestingStarted; uint256 vestingPlanId; } @@ -165,31 +164,15 @@ contract SymmioBuildersNftManager is VestingV2 { */ event UnlockCancelled(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner, uint256 amount); - /** - * @notice Emitted when the cliff period for an unlock request is completed. - * @param unlockId ID of the unlock request. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - */ - event CliffCompleted(uint256 indexed unlockId, uint256 indexed tokenId, address indexed owner); - /** * @notice Emitted when vesting starts for an unlock request. * @param unlockId ID of the unlock request. - * @param vestingPlanId ID of the created vesting plan. * @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 vestingPlanId, uint256 indexed tokenId, address owner, uint256 amount); - - /** - * @notice Emitted when an unlock process is completed for an NFT. - * @param tokenId ID of the NFT. - * @param owner Owner of the NFT. - * @param amount Amount of tokens to unlock. - */ - event UnlockCompleted(uint256 indexed tokenId, address indexed owner, uint256 amount); + event VestingStarted(uint256 indexed unlockId, uint256 indexed tokenId, address owner, uint256 amount, uint256 vestingPlanId); /** * @notice Emitted when the minimum lock amount is updated. @@ -305,9 +288,6 @@ contract SymmioBuildersNftManager is VestingV2 { // Mint new NFT tokenId = nftContract.mint(msg.sender, amount, brandName); - // Notify fee collectors - _notifyFeeCollectors(tokenId, int256(amount)); - emit NFTMinted(msg.sender, tokenId, amount, brandName); } @@ -331,9 +311,6 @@ contract SymmioBuildersNftManager is VestingV2 { // Mint new NFT tokenId = nftContract.mint(to, amount, brandName); - // Notify fee collectors - _notifyFeeCollectors(tokenId, int256(amount)); - emit NFTMintedWithoutBurn(msg.sender, to, tokenId, amount, brandName); } @@ -343,7 +320,6 @@ contract SymmioBuildersNftManager is VestingV2 { * @param amount Amount of SYMM tokens to lock. */ function lock(uint256 tokenId, uint256 amount) external nonReentrant whenNotPaused { - if (nftContract.ownerOf(tokenId) != msg.sender) revert NotTokenOwner(); if (amount == 0) revert ZeroAmount(); // Burn the SYMM tokens @@ -367,8 +343,7 @@ contract SymmioBuildersNftManager is VestingV2 { * @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) revert NotTokenOwner(); - if (nftContract.ownerOf(sourceTokenId) != msg.sender) revert NotTokenOwner(); + 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); @@ -399,12 +374,12 @@ contract SymmioBuildersNftManager is VestingV2 { */ 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(); - if (amount == 0) revert ZeroAmount(); // Update the unlocking amount nftContract.updateLockData(tokenId, data.amount, data.unlockingAmount + amount, data.name); @@ -416,7 +391,6 @@ contract SymmioBuildersNftManager is VestingV2 { unlockInitiatedTime: block.timestamp, owner: msg.sender, tokenId: tokenId, - cliffPassed: false, vestingStarted: false, vestingPlanId: 0 }); @@ -440,7 +414,7 @@ contract SymmioBuildersNftManager is VestingV2 { UnlockRequest storage request = unlockRequests[unlockId]; if (request.amount == 0) revert UnlockNotFound(); if (request.owner != msg.sender) revert NotTokenOwner(); - if (request.cliffPassed) revert CliffNotPassed(); + if (request.vestingStarted) revert VestingAlreadyStarted(); uint256 amount = request.amount; uint256 tokenId = request.tokenId; @@ -483,8 +457,7 @@ contract SymmioBuildersNftManager is VestingV2 { if (request.vestingStarted) revert VestingAlreadyStarted(); if (block.timestamp < request.unlockInitiatedTime + cliffDuration) revert CliffNotPassed(); - // Mark cliff as passed and vesting as started - request.cliffPassed = true; + // Mark vesting as started request.vestingStarted = true; // Complete unlock on NFT contract @@ -507,9 +480,7 @@ contract SymmioBuildersNftManager is VestingV2 { // Link vesting plan to unlock request request.vestingPlanId = planIds[0]; - emit CliffCompleted(unlockId, request.tokenId, request.owner); - emit VestingStarted(unlockId, planIds[0], request.tokenId, request.owner, request.amount); - emit UnlockCompleted(request.tokenId, request.owner, request.amount); + emit VestingStarted(unlockId, request.tokenId, request.owner, request.amount, planIds[0]); } /* ───────────────────── Cross-Chain Sync Functions ───────────────────── */ From 6f230bc97b898e8813abf8c9038acf2524113707 Mon Sep 17 00:00:00 2001 From: Naveed Date: Sat, 26 Jul 2025 11:37:43 +0200 Subject: [PATCH 43/48] Remove redundant methods --- .../builders-nft/SymmioBuildersNftManager.sol | 95 ++++--------------- 1 file changed, 18 insertions(+), 77 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 83afad9..8b00488 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -589,50 +589,26 @@ contract SymmioBuildersNftManager is VestingV2 { } /** - * @notice Get active unlock requests for a specific NFT. + * @notice Get unlock requests for a specific NFT. * @param tokenId ID of the NFT to query. - * @return Array of active UnlockRequest structs (excluding completed/vesting requests). - * - * @dev Filters out requests that have started vesting or been completed. + * @param start Start index. + * @param end End index. + * @param size Maximum number of requests to return. + * @return Array of unlock requests. */ - function getActiveUnlockRequests(uint256 tokenId) external view returns (UnlockRequest[] memory) { + function getUnlockedRequests(uint256 tokenId, uint256 start, uint256 end, uint256 size) external view returns (UnlockRequest[] memory) { uint256[] memory unlockIds = tokenUnlockIds[tokenId]; - uint256 activeCount = 0; + uint256 total = unlockIds.length; - // Count active requests (non-zero amount and not vesting) - for (uint256 i = 0; i < unlockIds.length; i++) { - if (unlockRequests[unlockIds[i]].amount > 0 && !unlockRequests[unlockIds[i]].vestingStarted) { - activeCount++; - } - } + if (end > total) end = total; + if (start > end) start = end; - // Populate active requests array - UnlockRequest[] memory activeRequests = new UnlockRequest[](activeCount); - uint256 index = 0; - for (uint256 i = 0; i < unlockIds.length; i++) { - UnlockRequest storage request = unlockRequests[unlockIds[i]]; - if (request.amount > 0 && !request.vestingStarted) { - activeRequests[index++] = request; - } - } - - return activeRequests; - } + uint256 count = end - start; + if (count > size) count = size; - /** - * @notice Check if an NFT has any active unlock requests. - * @param tokenId ID of the NFT to check. - * @return Whether the NFT has active unlock requests. - */ - function isUnlocking(uint256 tokenId) external view returns (bool) { - uint256[] memory unlockIds = tokenUnlockIds[tokenId]; - for (uint256 i = 0; i < unlockIds.length; i++) { - UnlockRequest storage request = unlockRequests[unlockIds[i]]; - if (request.amount > 0 && !request.vestingStarted) { - return true; - } - } - return false; + UnlockRequest[] memory requests = new UnlockRequest[](count); + for (uint256 i = 0; i < count; i++) requests[i] = unlockRequests[unlockIds[start + i]]; + return requests; } /** @@ -641,11 +617,8 @@ contract SymmioBuildersNftManager is VestingV2 { * @return Timestamp when the cliff period ends, or 0 if request is invalid. */ function getCliffEndTime(uint256 unlockId) external view returns (uint256) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return 0; - } - return request.unlockInitiatedTime + cliffDuration; + if (unlockId >= _unlockIdCounter) revert UnlockNotFound(); + return unlockRequests[unlockId].unlockInitiatedTime + cliffDuration; } /** @@ -654,40 +627,8 @@ contract SymmioBuildersNftManager is VestingV2 { * @return Whether the cliff period has passed. */ function isCliffPassed(uint256 unlockId) external view returns (bool) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return false; - } - return block.timestamp >= request.unlockInitiatedTime + cliffDuration; - } - - /** - * @notice Get the time remaining in the cliff period for an unlock request. - * @param unlockId ID of the unlock request. - * @return Seconds remaining until cliff period ends, or 0 if passed/invalid. - */ - function getCliffTimeRemaining(uint256 unlockId) external view returns (uint256) { - UnlockRequest storage request = unlockRequests[unlockId]; - if (request.amount == 0) { - return 0; - } - - uint256 cliffEndTime = request.unlockInitiatedTime + cliffDuration; - if (block.timestamp >= cliffEndTime) { - return 0; - } - - return cliffEndTime - block.timestamp; - } - - /** - * @notice Get the vesting plan ID for an unlock request. - * @param unlockId ID of the unlock request. - * @return vestingPlanId ID of the associated vesting plan (0 if not started). - */ - function getUnlockVestingPlanId(uint256 unlockId) external view returns (uint256 vestingPlanId) { - UnlockRequest storage request = unlockRequests[unlockId]; - return request.vestingPlanId; + if (unlockId >= _unlockIdCounter) revert UnlockNotFound(); + return block.timestamp >= unlockRequests[unlockId].unlockInitiatedTime + cliffDuration; } /* ───────────────────────── Internal Helpers ───────────────────────── */ From bcbcff1eec4f2ae0982d1f32e99e4787a71effa5 Mon Sep 17 00:00:00 2001 From: Naveed Date: Sat, 26 Jul 2025 14:13:25 +0200 Subject: [PATCH 44/48] Fix minor problems --- contracts/builders-nft/SymmioBuildersNft.sol | 2 ++ .../builders-nft/SymmioBuildersNftManager.sol | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/contracts/builders-nft/SymmioBuildersNft.sol b/contracts/builders-nft/SymmioBuildersNft.sol index 341e21e..c307e96 100644 --- a/contracts/builders-nft/SymmioBuildersNft.sol +++ b/contracts/builders-nft/SymmioBuildersNft.sol @@ -83,6 +83,7 @@ contract SymmioBuildersNft is 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 ─────────────────────────── */ @@ -265,6 +266,7 @@ contract SymmioBuildersNft is // 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); diff --git a/contracts/builders-nft/SymmioBuildersNftManager.sol b/contracts/builders-nft/SymmioBuildersNftManager.sol index 8b00488..e6d604b 100644 --- a/contracts/builders-nft/SymmioBuildersNftManager.sol +++ b/contracts/builders-nft/SymmioBuildersNftManager.sol @@ -349,19 +349,19 @@ contract SymmioBuildersNftManager is VestingV2 { ISymmioBuildersNft.LockData memory targetData = nftContract.getLockData(targetTokenId); ISymmioBuildersNft.LockData memory sourceData = nftContract.getLockData(sourceTokenId); - if (targetData.unlockingAmount > 0 || sourceData.unlockingAmount > 0) revert TokenHasActiveUnlock(); + if (sourceData.unlockingAmount > 0) revert TokenHasActiveUnlock(); // Merge locked amounts uint256 newAmount = targetData.amount + sourceData.amount; nftContract.updateLockData(targetTokenId, newAmount, targetData.unlockingAmount, targetData.name); - // Burn the source NFT and clear its data - nftContract.burn(sourceTokenId); - // 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); } @@ -460,14 +460,14 @@ contract SymmioBuildersNftManager is VestingV2 { // Mark vesting as started request.vestingStarted = true; - // Complete unlock on NFT contract ISymmioBuildersNft.LockData memory data = nftContract.getLockData(request.tokenId); - nftContract.updateLockData(request.tokenId, data.amount - request.amount, data.unlockingAmount - request.amount, data.name); + 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 - request.amount == 0 && data.unlockingAmount == 0) { - nftContract.burn(request.tokenId); - } + if (data.amount == 0 && newUnlockingAmount == 0) nftContract.burn(request.tokenId); // Create vesting plan using inherited Vesting functionality address[] memory users = new address[](1); From 75942af19a8f2654b2558f8d2657aa13e7ca4242 Mon Sep 17 00:00:00 2001 From: Naveed Date: Sat, 26 Jul 2025 14:19:16 +0200 Subject: [PATCH 45/48] Fix library usage pattern problem --- contracts/vesting/libraries/LibVestingPlan.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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; } } From 4ba6febf00d6aedd900c91d9bc72694d65b21e72 Mon Sep 17 00:00:00 2001 From: Naveed Date: Tue, 12 Aug 2025 17:15:59 +0200 Subject: [PATCH 46/48] Add staking reward notifier --- contracts/staking/ISymmStaking.sol | 7 + contracts/staking/RewardNotifier.sol | 197 +++++++++++++++++++++++++++ scripts/deployRewardNotifier.ts | 38 ++++++ 3 files changed, 242 insertions(+) create mode 100644 contracts/staking/ISymmStaking.sol create mode 100644 contracts/staking/RewardNotifier.sol create mode 100644 scripts/deployRewardNotifier.ts 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/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) + }) From 43e87498ba3fcbf5cf67c3334f1117018414c509 Mon Sep 17 00:00:00 2001 From: Naveed Date: Tue, 12 Aug 2025 17:16:39 +0200 Subject: [PATCH 47/48] Fix minor problem in vesting upsert manager --- contracts/vesting/VestingUpsertManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/vesting/VestingUpsertManager.sol b/contracts/vesting/VestingUpsertManager.sol index f86320c..0946e4d 100644 --- a/contracts/vesting/VestingUpsertManager.sol +++ b/contracts/vesting/VestingUpsertManager.sol @@ -46,7 +46,7 @@ contract VestingUpsertManager is Initializable, AccessControlEnumerableUpgradeab toSetupAmounts[setupCount] = newAmounts[i]; setupCount++; } else { - uint256 locked = vesting.getLockedAmountsForToken(token, users[i]); + uint256 locked = vesting.getLockedAmountsForToken(users[i], token); toReset[resetCount] = users[i]; toResetAmounts[resetCount] = locked + newAmounts[i]; resetCount++; From 47aff5d890fda93d7da6b55fcd78c2298c93484f Mon Sep 17 00:00:00 2001 From: timaster Date: Sun, 17 Aug 2025 12:45:25 +0200 Subject: [PATCH 48/48] Add BuildersNft tests --- tasks/index.ts | 4 +- tasks/symmVesting.ts | 151 ++--- tasks/symmVestingV2.ts | 153 ++--- tasks/symmioBuildersNft.ts | 29 + tasks/symmioBuildersNftManager.ts | 49 ++ tests/Initialize.fixture.ts | 111 ++-- tests/main.ts | 14 +- tests/symmioBuildersNft.behavior.ts | 136 +++++ tests/symmioBuildersNftManager.behavior.ts | 664 +++++++++++++++++++++ 9 files changed, 1083 insertions(+), 228 deletions(-) create mode 100644 tasks/symmioBuildersNft.ts create mode 100644 tasks/symmioBuildersNftManager.ts create mode 100644 tests/symmioBuildersNft.behavior.ts create mode 100644 tests/symmioBuildersNftManager.behavior.ts diff --git a/tasks/index.ts b/tasks/index.ts index 8c1a2f3..2dc01e0 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -4,4 +4,6 @@ import "./symmStaking" import "./symmAllocationClaimer" import "./symmVesting" import "./symmVestingPlanInitializerSetup" -import "./symmVestingV2" \ No newline at end of file +import "./symmVestingV2" +import "./symmioBuildersNft" +import "./symmioBuildersNftManager" \ No newline at end of file diff --git a/tasks/symmVesting.ts b/tasks/symmVesting.ts index ec9a66a..a6f0c47 100644 --- a/tasks/symmVesting.ts +++ b/tasks/symmVesting.ts @@ -1,4 +1,4 @@ -import { task, types } from "hardhat/config" +import { task, types } from "hardhat/config"; task("deploy:vesting", "Deploys the SymmVesting logic and proxy using CREATE2") .addParam("admin", "The admin of the SymmVesting contract") @@ -13,74 +13,54 @@ task("deploy:vesting", "Deploys the SymmVesting logic and proxy using CREATE2") .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 - - // 1. Deploy the VestingPlanOps library first - console.log("Deploying VestingPlanOps library...") - const VestingPlanOpsFactory = await ethers.getContractFactory("VestingPlanOps") - - // 2. Get an instance of your Create2Factory contract - const create2Factory = await ethers.getContractAt("Create2Factory", factory) - - // 3. Prepare library deployment bytecode - const libDeployTx = await VestingPlanOpsFactory.getDeployTransaction() - const libBytecode = libDeployTx.data - if (!libBytecode) { - throw new Error("Cannot obtain library deployment bytecode") - } - - // 4. Compute a deterministic salt for library - const librarySalt = ethers.keccak256(ethers.toUtf8Bytes(`library-vesting-planops`)) - console.log("Library salt:", librarySalt) - - // 5. Compute the predicted library address - const predictedLibAddress = await create2Factory.getFunction("getAddress")(libBytecode, librarySalt) - console.log("Predicted library address:", predictedLibAddress) - - if (!dryRun) { - // 6. Deploy the library via the factory using CREATE2 - console.log("Deploying library via CREATE2...") - const libTx = await create2Factory.deploy(libBytecode, librarySalt) - await libTx.wait() - console.log("Library deployed at:", predictedLibAddress) - } - - console.log() - - // 7. Get the contract factory for the logic contract with library linkage - const SymmVestingFactory = await ethers.getContractFactory("SymmVesting", { - libraries: { - VestingPlanOps: predictedLibAddress, - }, - }) - - // 8. Prepare implementation deployment bytecode - const implDeployTx = await SymmVestingFactory.getDeployTransaction() - const implBytecode = implDeployTx.data + .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") + throw new Error("Cannot obtain implementation deployment bytecode"); } - // 9. Compute a deterministic salt for implementation if not provided - const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vesting`)) - console.log("Implementation salt:", implementationSalt) + // - Compute a deterministic salt for implementation if not provided + const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vesting`)); + console.log("Implementation salt:", implementationSalt); - // 10. Compute the predicted implementation address - const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt) - console.log("Predicted implementation address:", predictedImplAddress) + // - Compute the predicted implementation address + const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt); + console.log("Predicted implementation address:", predictedImplAddress); if (!dryRun) { - // 11. 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() + // - 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(); } - // 12. Encode initializer data + // - Encode initializer data const initData = SymmVestingFactory.interface.encodeFunctionData("initialize", [ admin, penaltyreceiver, @@ -91,44 +71,39 @@ task("deploy:vesting", "Deploys the SymmVesting logic and proxy using CREATE2") symm, usdc, lp, - ]) - console.log("Deploying TransparentUpgradeableProxy with following params") - console.log(predictedImplAddress, admin, initData) + ]); + console.log("Deploying TransparentUpgradeableProxy with following params"); + console.log(predictedImplAddress, admin, initData); - // 13. Get the TransparentUpgradeableProxy factory - const TransparentUpgradeableProxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy") + // - 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 + const proxyDeployTx = await TransparentUpgradeableProxyFactory.getDeployTransaction(predictedImplAddress, admin, initData); + const proxyBytecode = proxyDeployTx.data; if (!proxyBytecode) { - throw new Error("Cannot obtain proxy deployment bytecode") + throw new Error("Cannot obtain proxy deployment bytecode"); } - // 15. Compute a deterministic salt for proxy if not provided - const proxySaltValue = proxysalt || ethers.keccak256(ethers.toUtf8Bytes(`proxy-vesting`)) - console.log("Proxy salt:", proxySaltValue) + // - 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) - // 16. Compute the predicted proxy address - const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue) - console.log("Predicted proxy address:", predictedProxyAddress) + // - Compute the predicted proxy address + const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue); + console.log("Predicted proxy address:", predictedProxyAddress); if (!dryRun) { - // 17. 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) + // - 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 { - // library: predictedLibAddress, - // implementation: predictedImplAddress, - // proxy: predictedProxyAddress, - // } - return await ethers.getContractAt("SymmVesting", predictedProxyAddress) - }) + return await ethers.getContractAt("SymmVesting", predictedProxyAddress); + }); diff --git a/tasks/symmVestingV2.ts b/tasks/symmVestingV2.ts index 6b4faee..eb0d151 100644 --- a/tasks/symmVestingV2.ts +++ b/tasks/symmVestingV2.ts @@ -1,4 +1,4 @@ -import { task, types } from "hardhat/config" +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") @@ -13,74 +13,54 @@ task("deploy:vestingV2", "Deploys the SymmVestingV2 logic and proxy using CREATE .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 - - // 1. Deploy the VestingPlanOps library first - console.log("Deploying VestingPlanOps library...") - const VestingPlanOpsFactory = await ethers.getContractFactory("VestingPlanOps") - - // 2. Get an instance of your Create2Factory contract - const create2Factory = await ethers.getContractAt("Create2Factory", factory) - - // 3. Prepare library deployment bytecode - const libDeployTx = await VestingPlanOpsFactory.getDeployTransaction() - const libBytecode = libDeployTx.data - if (!libBytecode) { - throw new Error("Cannot obtain library deployment bytecode") - } - - // 4. Compute a deterministic salt for library - const librarySalt = ethers.keccak256(ethers.toUtf8Bytes(`library-vesting-planops`)) - console.log("Library salt:", librarySalt) - - // 5. Compute the predicted library address - const predictedLibAddress = await create2Factory.getFunction("getAddress")(libBytecode, librarySalt) - console.log("Predicted library address:", predictedLibAddress) - - if (!dryRun) { - // 6. Deploy the library via the factory using CREATE2 - console.log("Deploying library via CREATE2...") - const libTx = await create2Factory.deploy(libBytecode, librarySalt) - await libTx.wait() - console.log("Library deployed at:", predictedLibAddress) - } - - console.log() - - // 7. Get the contract factory for the logic contract with library linkage - const SymmVestingFactory = await ethers.getContractFactory("SymmVestingV2", { - libraries: { - VestingPlanOps: predictedLibAddress, - }, - }) - - // 8. Prepare implementation deployment bytecode - const implDeployTx = await SymmVestingFactory.getDeployTransaction() - const implBytecode = implDeployTx.data + .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") + throw new Error("Cannot obtain implementation deployment bytecode"); } - // 9. Compute a deterministic salt for implementation if not provided - const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vestingV2`)) - console.log("Implementation salt:", implementationSalt) + // - Compute a deterministic salt for implementation if not provided + const implementationSalt = implsalt || ethers.keccak256(ethers.toUtf8Bytes(`vestingV2`)); + console.log("Implementation salt:", implementationSalt); - // 10. Compute the predicted implementation address - const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt) - console.log("Predicted implementation address:", predictedImplAddress) + // - Compute the predicted implementation address + const predictedImplAddress = await create2Factory.getFunction("getAddress")(implBytecode, implementationSalt); + console.log("Predicted implementation address:", predictedImplAddress); if (!dryRun) { - // 11. 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() + // - 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(); } - // 12. Encode initializer data + // - Encode initializer data const initData = SymmVestingFactory.interface.encodeFunctionData("initialize", [ admin, penaltyreceiver, @@ -91,44 +71,39 @@ task("deploy:vestingV2", "Deploys the SymmVestingV2 logic and proxy using CREATE symm, usdc, lp, - ]) - console.log("Deploying TransparentUpgradeableProxy with following params") - console.log(predictedImplAddress, admin, initData) + ]); + console.log("Deploying TransparentUpgradeableProxy with following params"); + console.log(predictedImplAddress, admin, initData); - // 13. Get the TransparentUpgradeableProxy factory - const TransparentUpgradeableProxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy") + // - Get the TransparentUpgradeableProxy factory + const TransparentUpgradeableProxyFactory = await ethers.getContractFactory("TransparentUpgradeableProxy"); - // 14. Prepare proxy deployment bytecode + // - Prepare proxy deployment bytecode // TransparentUpgradeableProxy constructor parameters: (logic, admin, data) - const proxyDeployTx = await TransparentUpgradeableProxyFactory.getDeployTransaction(predictedImplAddress, admin, initData) - const proxyBytecode = proxyDeployTx.data + const proxyDeployTx = await TransparentUpgradeableProxyFactory.getDeployTransaction(predictedImplAddress, admin, initData); + const proxyBytecode = proxyDeployTx.data; if (!proxyBytecode) { - throw new Error("Cannot obtain proxy deployment bytecode") + throw new Error("Cannot obtain proxy deployment bytecode"); } - // 15. Compute a deterministic salt for proxy if not provided - const proxySaltValue = proxysalt || ethers.keccak256(ethers.toUtf8Bytes(`proxy-vesting`)) - console.log("Proxy salt:", proxySaltValue) + // - 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) - // 16. Compute the predicted proxy address - const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue) - console.log("Predicted proxy address:", predictedProxyAddress) + // - Compute the predicted proxy address + const predictedProxyAddress = await create2Factory.getFunction("getAddress")(proxyBytecode, proxySaltValue); + console.log("Predicted proxy address:", predictedProxyAddress); if (!dryRun) { - // 17. 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) + // - 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 { - // library: predictedLibAddress, - // implementation: predictedImplAddress, - // proxy: predictedProxyAddress, - // } - return await ethers.getContractAt("SymmVestingV2", 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 71eb076..2897258 100644 --- a/tests/Initialize.fixture.ts +++ b/tests/Initialize.fixture.ts @@ -1,17 +1,14 @@ -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers" -import { ethers, run } from "hardhat" -import { e } from "../utils" +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { ethers, run } from "hardhat"; import { SymmAllocationClaimer, Symmio, - Vesting, + SymmioBuildersNft, + SymmioBuildersNftManager, SymmStaking, SymmVestingPlanInitializer, VestingV2, } from "../typechain-types"; -import * as Process from "process" -import { time } from "@nomicfoundation/hardhat-network-helpers" -import { floor } from "lodash" export class RunContext { signers!: { @@ -22,17 +19,19 @@ export class RunContext { user3: SignerWithAddress symmioFoundation: SignerWithAddress vestingPenaltyReceiver: SignerWithAddress - } - symmioToken!: Symmio - claimSymm!: SymmAllocationClaimer - vesting!: VestingV2 - symmStaking!: SymmStaking - symmVestingVlanInitializer!: SymmVestingPlanInitializer + }; + 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], @@ -41,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(), @@ -72,35 +71,51 @@ export async function initializeFixture(): Promise { // proxysalt: "2", // }) - 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.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.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.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(), @@ -110,10 +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.vesting.grantRole(await context.vesting.SETTER_ROLE(), await context.symmVestingVlanInitializer.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 19e9fae..d82b316 100644 --- a/tests/main.ts +++ b/tests/main.ts @@ -5,6 +5,8 @@ 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") { @@ -33,8 +35,16 @@ describe("Symmio Token", () => { // shouldBehaveLikeSymmVestingPlanInitializer() // }) - describe("Vesting V2", async function () { - ShouldBehaveLikeVestingV2() + // 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") { 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); + }); + }); + }); +}