mirror of
https://github.com/lens-protocol/core.git
synced 2026-04-22 03:02:03 -04:00
feat: (WIP) Initial setup of fork upgrade test, also removed use of freeCollectModule.
This commit is contained in:
43
contracts/mocks/MockCollectModule.sol
Normal file
43
contracts/mocks/MockCollectModule.sol
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity 0.8.15;
|
||||
|
||||
import {ICollectModule} from '../interfaces/ICollectModule.sol';
|
||||
|
||||
/**
|
||||
* @title FreeCollectModule
|
||||
* @author Lens Protocol
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
|
||||
*
|
||||
* This module works by allowing all collects.
|
||||
*/
|
||||
contract MockCollectModule is ICollectModule {
|
||||
/**
|
||||
* @dev There is nothing needed at initialization.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256,
|
||||
address,
|
||||
uint256,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
uint256 number = abi.decode(data, (uint256));
|
||||
require(number == 1, 'MockReferenceModule: invalid');
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Ensuring the collector is a follower, if needed
|
||||
*/
|
||||
function processCollect(
|
||||
uint256,
|
||||
uint256,
|
||||
address collector,
|
||||
address,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata
|
||||
) external view override {}
|
||||
}
|
||||
40
contracts/mocks/MockDeprecatedCollectModule.sol
Normal file
40
contracts/mocks/MockDeprecatedCollectModule.sol
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity 0.8.15;
|
||||
|
||||
import {IDeprecatedCollectModule} from '../interfaces/IDeprecatedCollectModule.sol';
|
||||
|
||||
/**
|
||||
* @title FreeCollectModule
|
||||
* @author Lens Protocol
|
||||
*
|
||||
* @notice This is a simple Lens CollectModule implementation, inheriting from the ICollectModule interface.
|
||||
*
|
||||
* This module works by allowing all collects.
|
||||
*/
|
||||
contract MockDeprecatedCollectModule is IDeprecatedCollectModule {
|
||||
/**
|
||||
* @dev There is nothing needed at initialization.
|
||||
*/
|
||||
function initializePublicationCollectModule(
|
||||
uint256,
|
||||
uint256,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
uint256 number = abi.decode(data, (uint256));
|
||||
require(number == 1, 'MockReferenceModule: invalid');
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Processes a collect by:
|
||||
* 1. Ensuring the collector is a follower, if needed
|
||||
*/
|
||||
function processCollect(
|
||||
uint256,
|
||||
address collector,
|
||||
uint256 profileId,
|
||||
uint256 pubId,
|
||||
bytes calldata
|
||||
) external view override {}
|
||||
}
|
||||
40
contracts/mocks/MockDeprecatedFollowModule.sol
Normal file
40
contracts/mocks/MockDeprecatedFollowModule.sol
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity 0.8.15;
|
||||
|
||||
import {IDeprecatedFollowModule} from '../interfaces/IDeprecatedFollowModule.sol';
|
||||
|
||||
/**
|
||||
* @dev This is a simple mock follow module to be used for testing.
|
||||
*/
|
||||
contract MockDeprecatedFollowModule is IDeprecatedFollowModule {
|
||||
function initializeFollowModule(
|
||||
uint256,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
uint256 number = abi.decode(data, (uint256));
|
||||
require(number == 1, 'MockFollowModule: invalid');
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
function processFollow(
|
||||
address follower,
|
||||
uint256 profileId,
|
||||
bytes calldata data
|
||||
) external override {}
|
||||
|
||||
function isFollowing(
|
||||
uint256,
|
||||
address,
|
||||
uint256
|
||||
) external pure override returns (bool) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function followModuleTransferHook(
|
||||
uint256 profileId,
|
||||
address from,
|
||||
address to,
|
||||
uint256 followNFTTokenId
|
||||
) external override {}
|
||||
}
|
||||
34
contracts/mocks/MockDeprecatedReferenceModule.sol
Normal file
34
contracts/mocks/MockDeprecatedReferenceModule.sol
Normal file
@@ -0,0 +1,34 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pragma solidity 0.8.15;
|
||||
|
||||
import {IDeprecatedReferenceModule} from '../interfaces/IDeprecatedReferenceModule.sol';
|
||||
|
||||
/**
|
||||
* @dev This is a simple mock follow module to be used for testing.
|
||||
*/
|
||||
contract MockDeprecatedReferenceModule is IDeprecatedReferenceModule {
|
||||
function initializeReferenceModule(
|
||||
uint256,
|
||||
uint256,
|
||||
bytes calldata data
|
||||
) external pure override returns (bytes memory) {
|
||||
uint256 number = abi.decode(data, (uint256));
|
||||
require(number == 1, 'MockReferenceModule: invalid');
|
||||
return new bytes(0);
|
||||
}
|
||||
|
||||
function processComment(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed,
|
||||
bytes calldata data
|
||||
) external override {}
|
||||
|
||||
function processMirror(
|
||||
uint256 profileId,
|
||||
uint256 profileIdPointed,
|
||||
uint256 pubIdPointed,
|
||||
bytes calldata data
|
||||
) external override {}
|
||||
}
|
||||
@@ -90,8 +90,8 @@ contract PublishingTest is BaseTest {
|
||||
bytes32 digest = _getPostTypedDataHash(
|
||||
firstProfileId,
|
||||
mockURI,
|
||||
address(freeCollectModule),
|
||||
abi.encode(false),
|
||||
address(mockCollectModule),
|
||||
abi.encode(1),
|
||||
address(0),
|
||||
'',
|
||||
nonce,
|
||||
@@ -104,8 +104,8 @@ contract PublishingTest is BaseTest {
|
||||
delegatedSigner: address(0),
|
||||
profileId: firstProfileId,
|
||||
contentURI: mockURI,
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: '',
|
||||
sig: _getSigStruct(otherSignerKey, digest, deadline)
|
||||
@@ -119,8 +119,8 @@ contract PublishingTest is BaseTest {
|
||||
bytes32 digest = _getPostTypedDataHash(
|
||||
firstProfileId,
|
||||
mockURI,
|
||||
address(freeCollectModule),
|
||||
abi.encode(false),
|
||||
address(mockCollectModule),
|
||||
abi.encode(1),
|
||||
address(0),
|
||||
'',
|
||||
nonce,
|
||||
@@ -133,8 +133,8 @@ contract PublishingTest is BaseTest {
|
||||
delegatedSigner: otherSigner,
|
||||
profileId: firstProfileId,
|
||||
contentURI: mockURI,
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: '',
|
||||
sig: _getSigStruct(otherSignerKey, digest, deadline)
|
||||
@@ -154,8 +154,8 @@ contract PublishingTest is BaseTest {
|
||||
firstProfileId,
|
||||
1,
|
||||
'',
|
||||
address(freeCollectModule),
|
||||
abi.encode(false),
|
||||
address(mockCollectModule),
|
||||
abi.encode(1),
|
||||
address(0),
|
||||
'',
|
||||
nonce,
|
||||
@@ -172,8 +172,8 @@ contract PublishingTest is BaseTest {
|
||||
profileIdPointed: firstProfileId,
|
||||
pubIdPointed: 1,
|
||||
referenceModuleData: '',
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: '',
|
||||
sig: sig
|
||||
@@ -193,8 +193,8 @@ contract PublishingTest is BaseTest {
|
||||
firstProfileId,
|
||||
1,
|
||||
'',
|
||||
address(freeCollectModule),
|
||||
abi.encode(false),
|
||||
address(mockCollectModule),
|
||||
abi.encode(1),
|
||||
address(0),
|
||||
'',
|
||||
nonce,
|
||||
@@ -211,8 +211,8 @@ contract PublishingTest is BaseTest {
|
||||
profileIdPointed: firstProfileId,
|
||||
pubIdPointed: 1,
|
||||
referenceModuleData: '',
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: '',
|
||||
sig: sig
|
||||
@@ -294,8 +294,8 @@ contract PublishingTest is BaseTest {
|
||||
bytes32 digest = _getPostTypedDataHash(
|
||||
firstProfileId,
|
||||
mockURI,
|
||||
address(freeCollectModule),
|
||||
abi.encode(false),
|
||||
address(mockCollectModule),
|
||||
abi.encode(1),
|
||||
address(0),
|
||||
'',
|
||||
nonce,
|
||||
@@ -307,8 +307,8 @@ contract PublishingTest is BaseTest {
|
||||
delegatedSigner: otherSigner,
|
||||
profileId: firstProfileId,
|
||||
contentURI: mockURI,
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: '',
|
||||
sig: _getSigStruct(otherSignerKey, digest, deadline)
|
||||
@@ -339,8 +339,8 @@ contract PublishingTest is BaseTest {
|
||||
firstProfileId,
|
||||
1,
|
||||
'',
|
||||
address(freeCollectModule),
|
||||
abi.encode(false),
|
||||
address(mockCollectModule),
|
||||
abi.encode(1),
|
||||
address(0),
|
||||
'',
|
||||
nonce,
|
||||
@@ -356,8 +356,8 @@ contract PublishingTest is BaseTest {
|
||||
profileIdPointed: firstProfileId,
|
||||
pubIdPointed: 1,
|
||||
referenceModuleData: '',
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: '',
|
||||
sig: sig
|
||||
|
||||
@@ -7,13 +7,13 @@ import 'forge-std/Test.sol';
|
||||
import '../../../contracts/core/LensHub.sol';
|
||||
import '../../../contracts/core/FollowNFT.sol';
|
||||
import '../../../contracts/core/CollectNFT.sol';
|
||||
import '../../../contracts/core/modules/collect/FreeCollectModule.sol';
|
||||
import '../../../contracts/upgradeability/TransparentUpgradeableProxy.sol';
|
||||
import '../../../contracts/libraries/DataTypes.sol';
|
||||
import '../../../contracts/libraries/Constants.sol';
|
||||
import '../../../contracts/libraries/Errors.sol';
|
||||
import '../../../contracts/libraries/GeneralLib.sol';
|
||||
import '../../../contracts/libraries/ProfileTokenURILogic.sol';
|
||||
import '../../../contracts/mocks/MockCollectModule.sol';
|
||||
|
||||
contract TestSetup is Test {
|
||||
uint256 constant firstProfileId = 1;
|
||||
@@ -36,7 +36,7 @@ contract TestSetup is Test {
|
||||
LensHub hubImpl;
|
||||
TransparentUpgradeableProxy hubAsProxy;
|
||||
LensHub hub;
|
||||
FreeCollectModule freeCollectModule;
|
||||
MockCollectModule mockCollectModule;
|
||||
|
||||
DataTypes.CreateProfileData mockCreateProfileData;
|
||||
|
||||
@@ -68,8 +68,8 @@ contract TestSetup is Test {
|
||||
// Cast proxy to LensHub interface.
|
||||
hub = LensHub(address(hubAsProxy));
|
||||
|
||||
// Deploy the FreeCollectModule.
|
||||
freeCollectModule = new FreeCollectModule(hubProxyAddr);
|
||||
// Deploy the MockCollectModule.
|
||||
mockCollectModule = new MockCollectModule();
|
||||
|
||||
// End deployments.
|
||||
vm.stopPrank();
|
||||
@@ -81,7 +81,7 @@ contract TestSetup is Test {
|
||||
hub.setState(DataTypes.ProtocolState.Unpaused);
|
||||
|
||||
// Whitelist the FreeCollectModule.
|
||||
hub.whitelistCollectModule(address(freeCollectModule), true);
|
||||
hub.whitelistCollectModule(address(mockCollectModule), true);
|
||||
|
||||
// Whitelist the test contract as a profile creator
|
||||
hub.whitelistProfileCreator(me, true);
|
||||
@@ -114,8 +114,8 @@ contract TestSetup is Test {
|
||||
mockPostData = DataTypes.PostData({
|
||||
profileId: firstProfileId,
|
||||
contentURI: mockURI,
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: ''
|
||||
});
|
||||
@@ -127,8 +127,8 @@ contract TestSetup is Test {
|
||||
profileIdPointed: firstProfileId,
|
||||
pubIdPointed: 1,
|
||||
referenceModuleData: '',
|
||||
collectModule: address(freeCollectModule),
|
||||
collectModuleInitData: abi.encode(false),
|
||||
collectModule: address(mockCollectModule),
|
||||
collectModuleInitData: abi.encode(1),
|
||||
referenceModule: address(0),
|
||||
referenceModuleInitData: ''
|
||||
});
|
||||
|
||||
@@ -2,12 +2,23 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol';
|
||||
import '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
import 'forge-std/console2.sol';
|
||||
import '../base/BaseTest.t.sol';
|
||||
import '../../../contracts/mocks/MockReferenceModule.sol';
|
||||
import '../../../contracts/mocks/MockDeprecatedReferenceModule.sol';
|
||||
import '../../../contracts/mocks/MockCollectModule.sol';
|
||||
import '../../../contracts/mocks/MockDeprecatedCollectModule.sol';
|
||||
import '../../../contracts/mocks/MockFollowModule.sol';
|
||||
import '../../../contracts/mocks/MockDeprecatedFollowModule.sol';
|
||||
import '../../../contracts/interfaces/IERC721Time.sol';
|
||||
|
||||
contract UpgradeForkTest is BaseTest {
|
||||
bytes32 constant ADMIN_SLOT = bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1);
|
||||
address constant POLYGON_HUB_PROXY = 0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d;
|
||||
address constant MUMBAI_HUB_PROXY = 0x60Ae865ee4C725cd04353b5AAb364553f56ceF82;
|
||||
|
||||
uint256 polygonForkId;
|
||||
uint256 mumbaiForkId;
|
||||
|
||||
@@ -31,6 +42,12 @@ contract UpgradeForkTest is BaseTest {
|
||||
address gov = oldHub.getGovernance();
|
||||
address proxyAdmin = address(uint160(uint256(vm.load(POLYGON_HUB_PROXY, ADMIN_SLOT))));
|
||||
|
||||
// Create a profile on the old hub, set the default profile.
|
||||
uint256 profileId = _fullCreateProfileSequence(gov, oldHub);
|
||||
|
||||
// Post, comment, mirror.
|
||||
_fullPublishSequence(profileId, gov, oldHub);
|
||||
|
||||
// Second, upgrade the hub.
|
||||
vm.prank(proxyAdmin);
|
||||
oldHubAsProxy.upgradeTo(address(hubImpl));
|
||||
@@ -43,4 +60,129 @@ contract UpgradeForkTest is BaseTest {
|
||||
oldHub.setGovernance(me);
|
||||
assertEq(oldHub.getGovernance(), me);
|
||||
}
|
||||
|
||||
function _fullPublishSequence(
|
||||
uint256 profileId,
|
||||
address gov,
|
||||
ILensHub hub
|
||||
) private {
|
||||
// In order to make this test suite evergreen, we must try publishing with a modern collect and reference
|
||||
// module since we don't know which version of the hub we're working with. If this fails, then we should
|
||||
// use deprecated modules.
|
||||
address mockReferenceModule = address(new MockReferenceModule());
|
||||
|
||||
vm.startPrank(gov);
|
||||
hub.whitelistCollectModule(address(mockCollectModule), true);
|
||||
hub.whitelistReferenceModule(mockReferenceModule, true);
|
||||
vm.stopPrank();
|
||||
|
||||
// Set the proper profile ID, reference module data, and profile ID pointed.
|
||||
mockPostData.profileId = profileId;
|
||||
mockPostData.referenceModuleInitData = abi.encode(1);
|
||||
mockCommentData.profileId = profileId;
|
||||
mockCommentData.profileIdPointed = profileId;
|
||||
mockCommentData.referenceModuleInitData = abi.encode(1);
|
||||
mockMirrorData.profileId = profileId;
|
||||
mockMirrorData.profileIdPointed = profileId;
|
||||
mockMirrorData.referenceModuleInitData = abi.encode(1);
|
||||
|
||||
// Set the modern reference module, the modern collect module is already set by default.
|
||||
mockPostData.referenceModule = mockReferenceModule;
|
||||
|
||||
try hub.post(mockPostData) returns (uint256 retPubId) {
|
||||
console2.log('Post published with modern collect and reference module.');
|
||||
uint256 postId = retPubId;
|
||||
assertEq(postId, 1);
|
||||
} catch {
|
||||
console2.log(
|
||||
'Post with modern collect and reference module failed, Attempting with deprecated modules'
|
||||
);
|
||||
|
||||
address mockDeprecatedCollectModule = address(new MockDeprecatedCollectModule());
|
||||
address mockDeprecatedReferenceModule = address(new MockDeprecatedReferenceModule());
|
||||
|
||||
vm.startPrank(gov);
|
||||
hub.whitelistCollectModule(mockDeprecatedCollectModule, true);
|
||||
hub.whitelistReferenceModule(mockDeprecatedReferenceModule, true);
|
||||
vm.stopPrank();
|
||||
|
||||
// Post.
|
||||
mockPostData.collectModule = mockDeprecatedCollectModule;
|
||||
mockPostData.referenceModule = mockDeprecatedReferenceModule;
|
||||
uint256 postId = hub.post(mockPostData);
|
||||
|
||||
// Validate post.
|
||||
assertEq(postId, 1);
|
||||
DataTypes.PublicationStruct memory pub = hub.getPub(profileId, postId);
|
||||
assertEq(pub.profileIdPointed, 0);
|
||||
assertEq(pub.pubIdPointed, 0);
|
||||
assertEq(pub.contentURI, mockPostData.contentURI);
|
||||
assertEq(pub.referenceModule, mockPostData.referenceModule);
|
||||
assertEq(pub.collectModule, mockPostData.collectModule);
|
||||
assertEq(pub.collectNFT, address(0));
|
||||
|
||||
// Comment.
|
||||
mockCommentData.collectModule = mockDeprecatedCollectModule;
|
||||
mockCommentData.referenceModule = mockDeprecatedReferenceModule;
|
||||
uint256 commentId = hub.comment(mockCommentData);
|
||||
|
||||
// Validate comment.
|
||||
assertEq(commentId, 2);
|
||||
pub = hub.getPub(profileId, commentId);
|
||||
assertEq(pub.profileIdPointed, mockCommentData.profileIdPointed);
|
||||
assertEq(pub.pubIdPointed, mockCommentData.pubIdPointed);
|
||||
assertEq(pub.contentURI, mockCommentData.contentURI);
|
||||
assertEq(pub.referenceModule, mockCommentData.referenceModule);
|
||||
assertEq(pub.collectModule, mockCommentData.collectModule);
|
||||
assertEq(pub.collectNFT, address(0));
|
||||
|
||||
// Mirror.
|
||||
mockMirrorData.referenceModule = mockDeprecatedReferenceModule;
|
||||
uint256 mirrorId = hub.mirror(mockMirrorData);
|
||||
|
||||
// Validate mirror.
|
||||
assertEq(mirrorId, 3);
|
||||
pub = hub.getPub(profileId, mirrorId);
|
||||
assertEq(pub.profileIdPointed, mockMirrorData.profileIdPointed);
|
||||
assertEq(pub.pubIdPointed, mockMirrorData.pubIdPointed);
|
||||
assertEq(pub.contentURI, '');
|
||||
assertEq(pub.referenceModule, mockMirrorData.referenceModule);
|
||||
assertEq(pub.collectModule, address(0));
|
||||
assertEq(pub.collectNFT, address(0));
|
||||
}
|
||||
}
|
||||
|
||||
function _fullCreateProfileSequence(address gov, ILensHub hub) private returns (uint256) {
|
||||
// In order to make this test suite evergreen, we must try setting a modern follow module since we don't know
|
||||
// which version of the hub we're working with, if this fails, then we should use a deprecated one.
|
||||
|
||||
address mockFollowModule = address(new MockFollowModule());
|
||||
vm.startPrank(gov);
|
||||
hub.whitelistProfileCreator(me, true);
|
||||
hub.whitelistFollowModule(mockFollowModule, true);
|
||||
vm.stopPrank();
|
||||
|
||||
mockCreateProfileData.to = me;
|
||||
mockCreateProfileData.handle = vm.toString(IERC721Enumerable(address(hub)).totalSupply());
|
||||
mockCreateProfileData.followModule = mockFollowModule;
|
||||
mockCreateProfileData.followModuleInitData = abi.encode(1);
|
||||
uint256 profileId;
|
||||
|
||||
try hub.createProfile(mockCreateProfileData) returns (uint256 retProfileId) {
|
||||
profileId = retProfileId;
|
||||
console2.log('Profile created with modern follow module.');
|
||||
} catch {
|
||||
console2.log(
|
||||
'Profile creation with modern follow module failed. Attempting with deprecated module.'
|
||||
);
|
||||
address mockDeprecatedFollowModule = address(new MockDeprecatedFollowModule());
|
||||
vm.prank(gov);
|
||||
hub.whitelistFollowModule(mockDeprecatedFollowModule, true);
|
||||
|
||||
mockCreateProfileData.followModule = mockDeprecatedFollowModule;
|
||||
profileId = hub.createProfile(mockCreateProfileData);
|
||||
}
|
||||
|
||||
return profileId;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user