update cec distributor
This commit is contained in:
parent
8d1bf19299
commit
d7c52b020f
3
.gitignore
vendored
3
.gitignore
vendored
@ -30,4 +30,5 @@ ignition/deployments/chain-31337
|
|||||||
/contracts/dist/
|
/contracts/dist/
|
||||||
/contracts/types/
|
/contracts/types/
|
||||||
openzeppelin
|
openzeppelin
|
||||||
imtbl
|
imtbl
|
||||||
|
bin
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404"
|
"solidity.compileUsingRemoteVersion": "v0.8.19+commit.7dd6d404",
|
||||||
|
"slither.solcPath": "",
|
||||||
|
"slither.hiddenDetectors": []
|
||||||
}
|
}
|
@ -17,18 +17,39 @@ contract CECDistributor is ReentrancyGuard, Pausable, Ownable, Governable {
|
|||||||
using SafeERC20 for IERC20;
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
mapping(address account => uint256 amount) public balanceMap;
|
mapping(address account => uint256 amount) public balanceMap;
|
||||||
// unlock time for this distributor
|
mapping(address account => uint256 amount) public releaseMap;
|
||||||
uint256 public unlockTime;
|
|
||||||
|
|
||||||
|
string public name;
|
||||||
IERC20 public immutable cecToken;
|
IERC20 public immutable cecToken;
|
||||||
|
uint256 public constant DURATION = 86400 * 30;
|
||||||
|
uint256 private releaseAllMonth;
|
||||||
|
|
||||||
|
uint256 public start = 0;
|
||||||
|
// release ratio when tge
|
||||||
|
uint256 public tgeRatio;
|
||||||
|
uint256 public constant TGE_PRECISION = 1000000;
|
||||||
|
//
|
||||||
|
uint256 public lockDuration;
|
||||||
|
address public wallet;
|
||||||
|
|
||||||
event EventBalanceUpdated(address indexed account, uint256 amount);
|
event EventBalanceUpdated(address indexed account, uint256 amount);
|
||||||
event EventUnlockTimeUpdated(uint256 unlockTime);
|
|
||||||
event EventCECClaimed(address indexed user, address indexed to, uint256 amount);
|
event EventCECClaimed(address indexed user, address indexed to, uint256 amount);
|
||||||
|
event EventChangeAddress(address oldAddr, address newAddr);
|
||||||
|
|
||||||
constructor(address _cecToken, uint256 _unlockTime) {
|
constructor(
|
||||||
|
string memory _name,
|
||||||
|
address _cecToken,
|
||||||
|
address _wallet,
|
||||||
|
uint256 _lockDuration,
|
||||||
|
uint256 _releaseAllMonth,
|
||||||
|
uint256 _tgeRatio
|
||||||
|
) {
|
||||||
|
name = _name;
|
||||||
cecToken = IERC20(_cecToken);
|
cecToken = IERC20(_cecToken);
|
||||||
unlockTime = _unlockTime;
|
wallet = _wallet;
|
||||||
|
lockDuration = _lockDuration;
|
||||||
|
releaseAllMonth = _releaseAllMonth;
|
||||||
|
tgeRatio = _tgeRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,7 +65,7 @@ contract CECDistributor is ReentrancyGuard, Pausable, Ownable, Governable {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @dev update pause state
|
* @dev update pause state
|
||||||
* When encountering special circumstances that require an emergency pause of the contract,
|
* When encountering special circumstances that require an emergency pause of the contract,
|
||||||
* the pause function can be called by the gov account to quickly pause the contract and minimize losses.
|
* the pause function can be called by the gov account to quickly pause the contract and minimize losses.
|
||||||
*/
|
*/
|
||||||
function pause() external ownerOrGov {
|
function pause() external ownerOrGov {
|
||||||
@ -58,9 +79,9 @@ contract CECDistributor is ReentrancyGuard, Pausable, Ownable, Governable {
|
|||||||
_unpause();
|
_unpause();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBalance(address account, uint256 amount) external onlyOwner {
|
function setStart(uint256 newStart) external ownerOrGov {
|
||||||
balanceMap[account] = amount;
|
require(newStart > 0 && start == 0, "CECDistributor: it's already initialized");
|
||||||
emit EventBalanceUpdated(account, amount);
|
start = newStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBalances(address[] calldata accounts, uint256[] calldata amounts) external onlyOwner {
|
function updateBalances(address[] calldata accounts, uint256[] calldata amounts) external onlyOwner {
|
||||||
@ -71,24 +92,49 @@ contract CECDistributor is ReentrancyGuard, Pausable, Ownable, Governable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUnlockTime(uint256 _unlockTime) external onlyOwner {
|
function calcClaimAmount(address user) public view whenNotPaused returns (uint256) {
|
||||||
unlockTime = _unlockTime;
|
require(balanceMap[user] > 0, "CECDistributor: not in whitelist");
|
||||||
emit EventUnlockTimeUpdated(_unlockTime);
|
require(block.timestamp >= start, "CECDistributor: not in claim time");
|
||||||
|
uint256 claimAmount = 0;
|
||||||
|
uint256 tgeAmount = 0;
|
||||||
|
if (tgeRatio > 0) {
|
||||||
|
tgeAmount = ((balanceMap[user] * tgeRatio) / TGE_PRECISION);
|
||||||
|
claimAmount += tgeAmount;
|
||||||
|
}
|
||||||
|
if (block.timestamp > start + lockDuration) {
|
||||||
|
uint256 monthNum = (block.timestamp - start - lockDuration) / DURATION;
|
||||||
|
if (monthNum <= releaseAllMonth) {
|
||||||
|
claimAmount += (((balanceMap[user] - tgeAmount) * monthNum) / releaseAllMonth);
|
||||||
|
} else {
|
||||||
|
claimAmount = balanceMap[user];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
claimAmount -= releaseMap[user];
|
||||||
|
return claimAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withdrawToken(address to, uint256 amount) external onlyOwner {
|
function claim(address to) external nonReentrant whenNotPaused returns (uint256) {
|
||||||
require(to != address(0), "CECDistributor: invalid address");
|
require(start > 0, "CECDistributor: start isn't init");
|
||||||
cecToken.safeTransfer(to, amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
function claim(address to) external nonReentrant whenNotPaused {
|
|
||||||
require(block.timestamp > unlockTime, "CECDistributor: not unlock time");
|
|
||||||
require(to != address(0), "CECDistributor: invalid address");
|
require(to != address(0), "CECDistributor: invalid address");
|
||||||
address _user = _msgSender();
|
address _user = _msgSender();
|
||||||
uint256 amount = balanceMap[_user];
|
uint256 amount = calcClaimAmount(_user);
|
||||||
require(amount > 0, "CECDistributor: no balance");
|
if (amount > 0) {
|
||||||
balanceMap[_user] = 0;
|
releaseMap[_user] = amount;
|
||||||
cecToken.safeTransfer(to, amount);
|
cecToken.safeTransferFrom(wallet, to, amount);
|
||||||
emit EventCECClaimed(_user, to, amount);
|
emit EventCECClaimed(_user, to, amount);
|
||||||
|
}
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeAddress(address from, address to) external {
|
||||||
|
require(balanceMap[to] == 0, "CECDistributor: new addr is in whitelist");
|
||||||
|
require(balanceMap[from] > 0, "CECDistributor: not in whitelist");
|
||||||
|
address _sender = _msgSender();
|
||||||
|
require(_sender == owner() || _sender == gov || _sender == from, "CECDistributor: sender not allowed");
|
||||||
|
balanceMap[to] = balanceMap[from];
|
||||||
|
balanceMap[from] = 0;
|
||||||
|
releaseMap[to] = releaseMap[from];
|
||||||
|
releaseMap[from] = 0;
|
||||||
|
emit EventChangeAddress(from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
contracts/core/CFTimelockController.sol
Normal file
12
contracts/core/CFTimelockController.sol
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity 0.8.19;
|
||||||
|
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
|
||||||
|
|
||||||
|
contract CFTimelockController is TimelockController {
|
||||||
|
constructor(
|
||||||
|
uint256 minDelay,
|
||||||
|
address[] memory proposers,
|
||||||
|
address[] memory executors,
|
||||||
|
address admin
|
||||||
|
) TimelockController(minDelay, proposers, executors, admin) {}
|
||||||
|
}
|
@ -13,7 +13,7 @@ contract Governable {
|
|||||||
_;
|
_;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setGov(address _gov) external onlyGov {
|
function setGov(address _gov) external virtual onlyGov {
|
||||||
gov = _gov;
|
gov = _gov;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
deploy/10_deploy_cecdistributor.ts
Normal file
36
deploy/10_deploy_cecdistributor.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import { DeployFunction } from "hardhat-deploy/types";
|
||||||
|
import { updateArray } from "../scripts/utils"
|
||||||
|
|
||||||
|
|
||||||
|
const deployCECDistributor: DeployFunction =
|
||||||
|
async function (hre: HardhatRuntimeEnvironment) {
|
||||||
|
const provider = hre.ethers.provider;
|
||||||
|
const from = await (await provider.getSigner()).getAddress();
|
||||||
|
const config = require(`../config/config_${hre.network.name}`);
|
||||||
|
const { admin, proposers, executors } = config.admins
|
||||||
|
const { cec } = config.staking
|
||||||
|
const params: any[] = [ cec, ]
|
||||||
|
const ret = await hre.deployments.deploy("CECDistributor", {
|
||||||
|
from,
|
||||||
|
args: params,
|
||||||
|
log: true,
|
||||||
|
});
|
||||||
|
console.log("==CECDistributor addr=", ret.address);
|
||||||
|
updateArray({
|
||||||
|
name: "CECDistributor",
|
||||||
|
type: "logic",
|
||||||
|
json: "assets/contracts/CECDistributor.json",
|
||||||
|
address: ret.address,
|
||||||
|
network: hre.network.name,
|
||||||
|
});
|
||||||
|
// verify the contract
|
||||||
|
await hre.run("verify:verify", {
|
||||||
|
address: ret.address,
|
||||||
|
constructorArguments: params,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deployCECDistributor.tags = ["CECDistributor"];
|
||||||
|
|
||||||
|
export default deployCECDistributor;
|
35
deploy/9_deploy_timelock.ts
Normal file
35
deploy/9_deploy_timelock.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||||
|
import { DeployFunction } from "hardhat-deploy/types";
|
||||||
|
import { updateArray } from "../scripts/utils"
|
||||||
|
|
||||||
|
|
||||||
|
const deployTimelock: DeployFunction =
|
||||||
|
async function (hre: HardhatRuntimeEnvironment) {
|
||||||
|
const provider = hre.ethers.provider;
|
||||||
|
const from = await (await provider.getSigner()).getAddress();
|
||||||
|
const config = require(`../config/config_${hre.network.name}`);
|
||||||
|
const { admin, proposers, executors } = config.admins
|
||||||
|
const params: any[] = [3600*24, proposers, executors, admin]
|
||||||
|
const ret = await hre.deployments.deploy("CFTimelockController", {
|
||||||
|
from,
|
||||||
|
args: params,
|
||||||
|
log: true,
|
||||||
|
});
|
||||||
|
console.log("==TimelockController addr=", ret.address);
|
||||||
|
updateArray({
|
||||||
|
name: "TimelockController",
|
||||||
|
type: "logic",
|
||||||
|
json: "assets/contracts/CFTimelockController.json",
|
||||||
|
address: ret.address,
|
||||||
|
network: hre.network.name,
|
||||||
|
});
|
||||||
|
// verify the contract
|
||||||
|
await hre.run("verify:verify", {
|
||||||
|
address: ret.address,
|
||||||
|
constructorArguments: params,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deployTimelock.tags = ["CFTimelockController"];
|
||||||
|
|
||||||
|
export default deployTimelock;
|
646
deployments/bsc_test/EsToken.json
Normal file
646
deployments/bsc_test/EsToken.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -17,12 +17,6 @@
|
|||||||
"json": "assets/contracts/GameItemMall.json",
|
"json": "assets/contracts/GameItemMall.json",
|
||||||
"address": "0xaE08adb5278B107D2501e7c61907e41FEf3887D7"
|
"address": "0xaE08adb5278B107D2501e7c61907e41FEf3887D7"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "TestCEC",
|
|
||||||
"type": "erc20",
|
|
||||||
"json": "assets/contracts/FT.json",
|
|
||||||
"address": "0xe34c5ea0c3083d11a735dc0609533b92130319f5"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "TokenClaim",
|
"name": "TokenClaim",
|
||||||
"type": "logic",
|
"type": "logic",
|
||||||
@ -76,5 +70,11 @@
|
|||||||
"type": "logic",
|
"type": "logic",
|
||||||
"json": "assets/contracts/RewardRouter.json",
|
"json": "assets/contracts/RewardRouter.json",
|
||||||
"address": "0x775d7Dbc06835c78437C8783fE11937E46F9ec6e"
|
"address": "0x775d7Dbc06835c78437C8783fE11937E46F9ec6e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TestCEC",
|
||||||
|
"type": "erc20",
|
||||||
|
"json": "assets/contracts/EsToken.json",
|
||||||
|
"address": "0xfa1223747bae6d519580c53Cbb9C11a45b13c6b7"
|
||||||
}
|
}
|
||||||
]
|
]
|
36
simple_abi/NFTLock.json
Normal file
36
simple_abi/NFTLock.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[
|
||||||
|
"constructor(uint256,address)",
|
||||||
|
"event DurationUpdated(uint256 indexed)",
|
||||||
|
"event Lock(address indexed,address indexed,address indexed,uint256[])",
|
||||||
|
"event OwnershipTransferred(address indexed,address indexed)",
|
||||||
|
"event Paused(address)",
|
||||||
|
"event UnLock(address indexed,address indexed,uint256,(uint256,address,bool)[])",
|
||||||
|
"event Unpaused(address)",
|
||||||
|
"event VerifierUpdated(address indexed)",
|
||||||
|
"function _CACHED_CHAIN_ID() view returns (uint256)",
|
||||||
|
"function _CACHED_THIS() view returns (address)",
|
||||||
|
"function addSupportNftList(address[])",
|
||||||
|
"function addressOriginal(address,uint256) view returns (address)",
|
||||||
|
"function checkSigner(address,bytes32,bytes) pure",
|
||||||
|
"function duration() view returns (uint256)",
|
||||||
|
"function getMessageHash(address,address,(uint256,address,bool)[],address,uint256,uint256,uint256) pure returns (bytes32)",
|
||||||
|
"function lock(address,address,uint256[])",
|
||||||
|
"function lockedNft(address,address) view returns (uint256[])",
|
||||||
|
"function lockedNum(address,address) view returns (uint256)",
|
||||||
|
"function maxBatch() view returns (uint256)",
|
||||||
|
"function minDuration() view returns (uint256)",
|
||||||
|
"function onERC721Received(address,address,uint256,bytes) returns (bytes4)",
|
||||||
|
"function owner() view returns (address)",
|
||||||
|
"function passportOriginal(address,uint256) view returns (address)",
|
||||||
|
"function paused() view returns (bool)",
|
||||||
|
"function removeSupportNft(address)",
|
||||||
|
"function renounceOwnership()",
|
||||||
|
"function supportNftList(address) view returns (bool)",
|
||||||
|
"function transferOwnership(address)",
|
||||||
|
"function unlockOrMint(address,(uint256,address,bool)[],uint256,uint256,bytes)",
|
||||||
|
"function unlockWithSvr(address,uint256[])",
|
||||||
|
"function updateBatch(uint256)",
|
||||||
|
"function updateDuation(uint256)",
|
||||||
|
"function updateVerifier(address)",
|
||||||
|
"function verifier() view returns (address)"
|
||||||
|
]
|
112
test/testCECDistributor.ts
Normal file
112
test/testCECDistributor.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import hre from "hardhat";
|
||||||
|
import {
|
||||||
|
getBytes,
|
||||||
|
solidityPackedKeccak256,
|
||||||
|
} from 'ethers'
|
||||||
|
import {
|
||||||
|
loadFixture,
|
||||||
|
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
|
||||||
|
import { expandDecimals, increaseTime, mineBlock, print } from './shared/utilities';
|
||||||
|
|
||||||
|
const ONE_DAY = 3600 * 24
|
||||||
|
const ONE_MONTH = ONE_DAY * 30
|
||||||
|
|
||||||
|
describe('TestCECDistributor', function() {
|
||||||
|
async function deployOneContract() {
|
||||||
|
// Contracts are deployed using the first signer/account by default
|
||||||
|
const [owner, user0, user1, user2] = await hre.ethers.getSigners();
|
||||||
|
const Cec = await hre.ethers.getContractFactory("MintableBaseToken");
|
||||||
|
const cec = await Cec.deploy("test cec", "cec");
|
||||||
|
await cec.setMinter(owner.address, true)
|
||||||
|
await cec.mint(user0.address, expandDecimals(15000, 18))
|
||||||
|
|
||||||
|
const CECDistributor = await hre.ethers.getContractFactory("CECDistributor");
|
||||||
|
const lockDuration = ONE_DAY; // one day
|
||||||
|
const distributor = await CECDistributor.deploy("first", cec.target, user0.address, lockDuration, 10, 300000 );
|
||||||
|
//@ts-ignore
|
||||||
|
await cec.connect(user0).approve(distributor.target, expandDecimals(10000, 18))
|
||||||
|
const chainId = hre.network.config.chainId
|
||||||
|
const start = (Date.now() / 1000 + 3600) | 0 // one hour later
|
||||||
|
await distributor.setStart(start)
|
||||||
|
expect(await distributor.name()).to.equal("first")
|
||||||
|
await distributor.updateBalances([user1.address], [expandDecimals(1000, 18)])
|
||||||
|
return { distributor, owner, user0, user1, user2, chainId, cec, start };
|
||||||
|
}
|
||||||
|
describe("Deployment", function () {
|
||||||
|
it('should deploy CECDistributor', async function() {
|
||||||
|
const { distributor, user0, user1, user2, cec } = await loadFixture(deployOneContract);
|
||||||
|
expect(await distributor.name()).to.equal("first")
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should success claim', async function() {
|
||||||
|
const { distributor, owner, user0, user1, user2, cec, start } = await loadFixture(deployOneContract);
|
||||||
|
const wallet = owner
|
||||||
|
const provider = wallet.provider;
|
||||||
|
await increaseTime(provider, 3601)
|
||||||
|
await mineBlock(provider)
|
||||||
|
const claimAmount1 = await distributor.calcClaimAmount(user1.address)
|
||||||
|
expect(claimAmount1).to.equal(expandDecimals(300, 18))
|
||||||
|
// @ts-ignore
|
||||||
|
await distributor.connect(user1).claim(user1.address)
|
||||||
|
expect(await cec.balanceOf(user1.address)).to.equal(expandDecimals(300, 18))
|
||||||
|
|
||||||
|
const claimAmount2 = await distributor.calcClaimAmount(user1.address)
|
||||||
|
expect(claimAmount2).to.equal(0)
|
||||||
|
|
||||||
|
await increaseTime(provider, ONE_DAY)
|
||||||
|
await mineBlock(provider)
|
||||||
|
const claimAmount3 = await distributor.calcClaimAmount(user1.address)
|
||||||
|
expect(claimAmount3).to.equal(0)
|
||||||
|
|
||||||
|
await increaseTime(provider, ONE_MONTH)
|
||||||
|
await mineBlock(provider)
|
||||||
|
|
||||||
|
const claimAmount4 = await distributor.calcClaimAmount(user1.address)
|
||||||
|
expect(claimAmount4).to.equal(expandDecimals(70, 18))
|
||||||
|
|
||||||
|
await increaseTime(provider, ONE_MONTH)
|
||||||
|
await mineBlock(provider)
|
||||||
|
|
||||||
|
const claimAmount5 = await distributor.calcClaimAmount(user1.address)
|
||||||
|
expect(claimAmount5).to.equal(expandDecimals(140, 18))
|
||||||
|
// @ts-ignore
|
||||||
|
await distributor.connect(user1).changeAddress(user1.address, user2.address)
|
||||||
|
const claimAmount6 = await distributor.calcClaimAmount(user2.address)
|
||||||
|
expect(claimAmount6).to.equal(expandDecimals(140, 18))
|
||||||
|
|
||||||
|
await expect(distributor.calcClaimAmount(user1.address)).to.be.revertedWith("CECDistributor: not in whitelist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should revert claim for not start', async function() {
|
||||||
|
const { distributor, owner, user0, user1, user2, cec, start } = await loadFixture(deployOneContract);
|
||||||
|
// @ts-ignore
|
||||||
|
await expect(distributor.connect(user1).claim(user1.address)).to.be.revertedWith("CECDistributor: not in claim time");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should revert claim for not in whitelist', async function() {
|
||||||
|
const { distributor, owner, user0, user1, user2, cec, start } = await loadFixture(deployOneContract);
|
||||||
|
// @ts-ignore
|
||||||
|
await expect(distributor.connect(user2).claim(user1.address)).to.be.revertedWith("CECDistributor: not in whitelist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should revert claim for pause', async function() {
|
||||||
|
const { distributor, owner, user0, user1, user2, cec, start } = await loadFixture(deployOneContract);
|
||||||
|
await distributor.pause()
|
||||||
|
// @ts-ignore
|
||||||
|
await expect(distributor.connect(user2).claim(user1.address)).to.be.revertedWith("Pausable: paused");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change gov success', async function() {
|
||||||
|
const { distributor, owner, user0, user1, user2, cec, start } = await loadFixture(deployOneContract);
|
||||||
|
await distributor.setGov(user2.address)
|
||||||
|
expect(await distributor.gov()).to.equal(user2.address)
|
||||||
|
// @ts-ignore
|
||||||
|
await distributor.connect(user2).pause()
|
||||||
|
expect(await distributor.paused()).to.equal(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user