Merge branch 'main' into feat/collect-modules-toggle-follower-only

This commit is contained in:
Peter Michael
2022-03-31 19:05:15 -04:00
30 changed files with 708 additions and 154 deletions

View File

@@ -2,6 +2,8 @@ module.exports = {
skipFiles: [
'/core/base/ERC721Time.sol',
'/core/base/ERC721Enumerable.sol',
'/core/modules/follow/SecretCodeFollowModule.sol',
'/upgradeability',
'/interfaces',
'/mocks',
],

View File

@@ -28,6 +28,7 @@ contract CollectNFT is ICollectNFT, LensNFTBase {
// to initialize the hub proxy at construction.
constructor(address hub) {
HUB = hub;
_initialized = true;
}
/// @inheritdoc ICollectNFT

View File

@@ -9,7 +9,6 @@ import {Errors} from '../libraries/Errors.sol';
import {Events} from '../libraries/Events.sol';
import {DataTypes} from '../libraries/DataTypes.sol';
import {LensNFTBase} from './base/LensNFTBase.sol';
import {IERC721Metadata} from '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol';
/**
* @title FollowNFT
@@ -47,6 +46,7 @@ contract FollowNFT is LensNFTBase, IFollowNFT {
// We create the FollowNFT with the pre-computed HUB address before deploying the hub.
constructor(address hub) {
HUB = hub;
_initialized = true;
}
/// @inheritdoc IFollowNFT

View File

@@ -940,7 +940,7 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
address from,
address to,
uint256 tokenId
) internal override {
) internal override whenNotPaused {
if (_dispatcherByProfile[tokenId] != address(0)) {
_setDispatcher(tokenId, address(0));
}

View File

@@ -9,8 +9,10 @@ import {Errors} from '../../libraries/Errors.sol';
/**
* @title LensMultiState
*
* @notice This is an abstract contract that implements internal LensHub state setting and
* validation.
* @notice This is an abstract contract that implements internal LensHub state setting and validation.
*
* whenNotPaused: Either publishingPaused or Unpaused.
* whenPublishingEnabled: When Unpaused only.
*/
abstract contract LensMultiState {
DataTypes.ProtocolState private _state;
@@ -39,9 +41,7 @@ abstract contract LensMultiState {
}
function _validatePublishingEnabled() internal view {
if (_state == DataTypes.ProtocolState.Paused) {
revert Errors.Paused();
} else if (_state == DataTypes.ProtocolState.PublishingPaused) {
if (_state != DataTypes.ProtocolState.Unpaused) {
revert Errors.PublishingPaused();
}
}

View File

@@ -75,7 +75,7 @@ contract FeeCollectModule is ICollectModule, FeeModuleBase, FollowValidationModu
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount < BPS_MAX
amount == 0
) revert Errors.InitParamsInvalid();
_dataByPublicationByProfile[profileId][pubId].amount = amount;
@@ -141,7 +141,8 @@ contract FeeCollectModule is ICollectModule, FeeModuleBase, FollowValidationModu
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
function _processCollectWithReferral(
@@ -181,6 +182,7 @@ contract FeeCollectModule is ICollectModule, FeeModuleBase, FollowValidationModu
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -80,7 +80,7 @@ contract LimitedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidat
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount < BPS_MAX
amount == 0
) revert Errors.InitParamsInvalid();
_dataByPublicationByProfile[profileId][pubId].collectLimit = collectLimit;
@@ -156,7 +156,8 @@ contract LimitedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidat
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
function _processCollectWithReferral(
@@ -196,6 +197,7 @@ contract LimitedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidat
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -87,7 +87,7 @@ contract LimitedTimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowVa
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount < BPS_MAX
amount == 0
) revert Errors.InitParamsInvalid();
_dataByPublicationByProfile[profileId][pubId].collectLimit = collectLimit;
@@ -168,7 +168,8 @@ contract LimitedTimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowVa
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
function _processCollectWithReferral(
@@ -208,6 +209,7 @@ contract LimitedTimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowVa
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -84,7 +84,7 @@ contract TimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidatio
!_currencyWhitelisted(currency) ||
recipient == address(0) ||
referralFee > BPS_MAX ||
amount < BPS_MAX
amount == 0
) revert Errors.InitParamsInvalid();
_dataByPublicationByProfile[profileId][pubId].amount = amount;
@@ -155,7 +155,8 @@ contract TimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidatio
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
function _processCollectWithReferral(
@@ -195,6 +196,7 @@ contract TimedFeeCollectModule is ICollectModule, FeeModuleBase, FollowValidatio
address recipient = _dataByPublicationByProfile[profileId][pubId].recipient;
IERC20(currency).safeTransferFrom(collector, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(collector, treasury, treasuryAmount);
}
}

View File

@@ -59,7 +59,7 @@ contract FeeFollowModule is IFollowModule, FeeModuleBase, FollowValidatorFollowM
data,
(uint256, address, address)
);
if (!_currencyWhitelisted(currency) || recipient == address(0) || amount < BPS_MAX)
if (!_currencyWhitelisted(currency) || recipient == address(0) || amount == 0)
revert Errors.InitParamsInvalid();
_dataByProfile[profileId].amount = amount;
@@ -87,7 +87,8 @@ contract FeeFollowModule is IFollowModule, FeeModuleBase, FollowValidatorFollowM
uint256 adjustedAmount = amount - treasuryAmount;
IERC20(currency).safeTransferFrom(follower, recipient, adjustedAmount);
IERC20(currency).safeTransferFrom(follower, treasury, treasuryAmount);
if (treasuryAmount > 0)
IERC20(currency).safeTransferFrom(follower, treasury, treasuryAmount);
}
/**

View File

@@ -11,7 +11,6 @@ import {IFollowNFT} from '../interfaces/IFollowNFT.sol';
import {ICollectNFT} from '../interfaces/ICollectNFT.sol';
import {IFollowModule} from '../interfaces/IFollowModule.sol';
import {ICollectModule} from '../interfaces/ICollectModule.sol';
import {ERC721Time} from '../core/base/ERC721Time.sol';
import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
@@ -35,6 +34,7 @@ library InteractionLogic {
* @param followModuleDatas The array of follow module data parameters to pass to each profile's follow module.
* @param followNFTImpl The address of the follow NFT implementation, which has to be passed because it's an immutable in the hub.
* @param _profileById A pointer to the storage mapping of profile structs by profile ID.
* @param _profileIdByHandleHash A pointer to the storage mapping of profile IDs by handle hash.
*/
function follow(
address follower,
@@ -47,7 +47,7 @@ library InteractionLogic {
if (profileIds.length != followModuleDatas.length) revert Errors.ArrayMismatch();
for (uint256 i = 0; i < profileIds.length; ++i) {
string memory handle = _profileById[profileIds[i]].handle;
if (_profileIdByHandleHash[keccak256(bytes(handle))] == 0)
if (_profileIdByHandleHash[keccak256(bytes(handle))] != profileIds[i])
revert Errors.TokenDoesNotExist();
address followModule = _profileById[profileIds[i]].followModule;

File diff suppressed because one or more lines are too long

View File

@@ -404,7 +404,10 @@ library PublishingLogic {
if (
(byteHandle[i] < '0' ||
byteHandle[i] > 'z' ||
(byteHandle[i] > '9' && byteHandle[i] < 'a')) && byteHandle[i] != '.'
(byteHandle[i] > '9' && byteHandle[i] < 'a')) &&
byteHandle[i] != '.' &&
byteHandle[i] != '-' &&
byteHandle[i] != '_'
) revert Errors.HandleContainsInvalidCharacters();
}
}

View File

@@ -2,8 +2,6 @@ version: '3.5'
services:
contracts-env:
security_opt:
- no-new-privileges
user: "${USERID?}:${USERID?}"
env_file:
- .env

View File

@@ -222,14 +222,15 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
[lensHub.address, moduleGlobals.address],
'contracts/core/modules/follow/FeeFollowModule.sol:FeeFollowModule'
);
console.log('\n\t-- Deploying approvalFollowModule --');
const approvalFollowModule = await deployWithVerify(
new ApprovalFollowModule__factory(deployer).deploy(lensHub.address, {
nonce: deployerNonce++,
}),
[lensHub.address],
'contracts/core/modules/follow/ApprovalFollowModule.sol:ApprovalFollowModule'
);
// --- COMMENTED OUT AS THIS IS NOT A LAUNCH MODULE ---
// console.log('\n\t-- Deploying approvalFollowModule --');
// const approvalFollowModule = await deployWithVerify(
// new ApprovalFollowModule__factory(deployer).deploy(lensHub.address, {
// nonce: deployerNonce++,
// }),
// [lensHub.address],
// 'contracts/core/modules/follow/ApprovalFollowModule.sol:ApprovalFollowModule'
// );
// Deploy reference module
console.log('\n\t-- Deploying followerOnlyReferenceModule --');
@@ -276,11 +277,12 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
await waitForTx(
lensHub.whitelistFollowModule(feeFollowModule.address, true, { nonce: governanceNonce++ })
);
await waitForTx(
lensHub.whitelistFollowModule(approvalFollowModule.address, true, {
nonce: governanceNonce++,
})
);
// --- COMMENTED OUT AS THIS IS NOT A LAUNCH MODULE ---
// await waitForTx(
// lensHub.whitelistFollowModule(approvalFollowModule.address, true, {
// nonce: governanceNonce++,
// })
// );
// Whitelist the reference module
console.log('\n\t-- Whitelisting Reference Module --');
@@ -308,7 +310,8 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
'revert collect module': revertCollectModule.address,
'empty collect module': freeCollectModule.address,
'fee follow module': feeFollowModule.address,
'approval follow module': approvalFollowModule.address,
// --- COMMENTED OUT AS THIS IS NOT A LAUNCH MODULE ---
// 'approval follow module': approvalFollowModule.address,
'follower only reference module': followerOnlyReferenceModule.address,
};
const json = JSON.stringify(addrs, null, 2);

View File

@@ -175,10 +175,11 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
nonce: deployerNonce++,
})
);
console.log('\n\t-- Deploying approvalFollowModule --');
const approvalFollowModule = await deployContract(
new ApprovalFollowModule__factory(deployer).deploy(lensHub.address, { nonce: deployerNonce++ })
);
// --- COMMENTED OUT AS THIS IS NOT A LAUNCH MODULE ---
// console.log('\n\t-- Deploying approvalFollowModule --');
// const approvalFollowModule = await deployContract(
// new ApprovalFollowModule__factory(deployer).deploy(lensHub.address, { nonce: deployerNonce++ })
// );
// Deploy reference module
console.log('\n\t-- Deploying followerOnlyReferenceModule --');
@@ -221,9 +222,10 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
await waitForTx(
lensHub.whitelistFollowModule(feeFollowModule.address, true, { nonce: governanceNonce++ })
);
await waitForTx(
lensHub.whitelistFollowModule(approvalFollowModule.address, true, { nonce: governanceNonce++ })
);
// --- COMMENTED OUT AS THIS IS NOT A LAUNCH MODULE ---
// await waitForTx(
// lensHub.whitelistFollowModule(approvalFollowModule.address, true, { nonce: governanceNonce++ })
// );
// Whitelist the reference module
console.log('\n\t-- Whitelisting Reference Module --');
@@ -259,7 +261,8 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
'revert collect module': revertCollectModule.address,
'empty collect module': freeCollectModule.address,
'fee follow module': feeFollowModule.address,
'approval follow module': approvalFollowModule.address,
// --- COMMENTED OUT AS THIS IS NOT A LAUNCH MODULE ---
// 'approval follow module': approvalFollowModule.address,
'follower only reference module': followerOnlyReferenceModule.address,
};
const json = JSON.stringify(addrs, null, 2);

View File

@@ -15,6 +15,8 @@ import { hexlify, keccak256, RLP, toUtf8Bytes } from 'ethers/lib/utils';
import { LensHub__factory } from '../../typechain-types';
import { TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
import hre, { ethers } from 'hardhat';
import { readFileSync } from 'fs';
import { join } from 'path';
export enum ProtocolState {
Unpaused,
@@ -26,16 +28,17 @@ export function matchEvent(
receipt: TransactionReceipt,
name: string,
expectedArgs?: any[],
eventContract: Contract = eventsLib
eventContract: Contract = eventsLib,
emitterAddress?: string
) {
const events = receipt.logs;
if (events != undefined) {
// match name from list of events in eventContract, when found, compute the sigHash
let sigHash: string | undefined;
for (let contractEvents of Object.keys(eventContract.interface.events)) {
if (contractEvents.startsWith(name) && contractEvents.charAt(name.length) == '(') {
sigHash = keccak256(toUtf8Bytes(contractEvents));
for (let contractEvent of Object.keys(eventContract.interface.events)) {
if (contractEvent.startsWith(name) && contractEvent.charAt(name.length) == '(') {
sigHash = keccak256(toUtf8Bytes(contractEvent));
break;
}
}
@@ -51,6 +54,10 @@ export function matchEvent(
for (let emittedEvent of events) {
// If we find one with the correct sighash, check if it is the one we're looking for
if (emittedEvent.topics[0] == sigHash) {
// If an emitter address is passed, validate that this is indeed the correct emitter, if not, continue
if (emitterAddress) {
if (emittedEvent.address != emitterAddress) continue;
}
const event = eventContract.interface.parseLog(emittedEvent);
// If there are expected arguments, validate them, otherwise, return here
if (expectedArgs) {
@@ -106,7 +113,9 @@ export function matchEvent(
if (invalidParamsButExists) {
logger.throwError(`Event "${name}" found in logs but with unexpected args`);
} else {
logger.throwError(`Event "${name}" not found in given transaction log`);
logger.throwError(
`Event "${name}" not found emitted by "${emitterAddress}" in given transaction log`
);
}
} else {
logger.throwError('No events were emitted');
@@ -447,7 +456,21 @@ export async function getCollectWithSigParts(
return await getSig(msgParams);
}
export async function getJsonMetadataFromBase64TokenUri(tokenUri: string) {
export interface TokenUriMetadataAttribute {
trait_type: string;
value: string;
}
export interface ProfileTokenUriMetadata {
name: string;
description: string;
image: string;
attributes: TokenUriMetadataAttribute[];
}
export async function getMetadataFromBase64TokenUri(
tokenUri: string
): Promise<ProfileTokenUriMetadata> {
const splittedTokenUri = tokenUri.split('data:application/json;base64,');
if (splittedTokenUri.length != 2) {
logger.throwError('Wrong or unrecognized token URI format');
@@ -459,6 +482,19 @@ export async function getJsonMetadataFromBase64TokenUri(tokenUri: string) {
}
}
export async function getDecodedSvgImage(tokenUriMetadata: ProfileTokenUriMetadata) {
const splittedImage = tokenUriMetadata.image.split('data:image/svg+xml;base64,');
if (splittedImage.length != 2) {
logger.throwError('Wrong or unrecognized token URI format');
} else {
return ethers.utils.toUtf8String(ethers.utils.base64.decode(splittedImage[1]));
}
}
export function loadTestResourceAsUtf8String(relativePathToResouceDir: string) {
return readFileSync(join('test', 'resources', relativePathToResouceDir), 'utf8');
}
// Modified from AaveTokenV2 repo
const buildPermitParams = (
nft: string,

View File

@@ -97,6 +97,24 @@ makeSuiteCleanRoom('Multi-State Hub', function () {
context('Paused State', function () {
context('Scenarios', async function () {
it('User should create a profile, governance should pause the hub, transferring the profile should fail', async function () {
await expect(
lensHub.createProfile({
to: userAddress,
handle: MOCK_PROFILE_HANDLE,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(lensHub.connect(governance).setState(ProtocolState.Paused)).to.not.be.reverted;
await expect(
lensHub.transferFrom(userAddress, userTwoAddress, FIRST_PROFILE_ID)
).to.be.revertedWith(ERRORS.PAUSED);
});
it('Governance should pause the hub, profile creation should fail, then governance unpauses the hub and profile creation should work', async function () {
await expect(lensHub.connect(governance).setState(ProtocolState.Paused)).to.not.be.reverted;
@@ -472,7 +490,7 @@ makeSuiteCleanRoom('Multi-State Hub', function () {
referenceModule: ZERO_ADDRESS,
referenceModuleData: [],
})
).to.be.revertedWith(ERRORS.PAUSED);
).to.be.revertedWith(ERRORS.PUBLISHING_PAUSED);
await expect(
lensHub.connect(governance).setState(ProtocolState.Unpaused)
@@ -538,7 +556,7 @@ makeSuiteCleanRoom('Multi-State Hub', function () {
deadline: MAX_UINT256,
},
})
).to.be.revertedWith(ERRORS.PAUSED);
).to.be.revertedWith(ERRORS.PUBLISHING_PAUSED);
await expect(
lensHub.connect(governance).setState(ProtocolState.Unpaused)
@@ -602,7 +620,7 @@ makeSuiteCleanRoom('Multi-State Hub', function () {
referenceModule: ZERO_ADDRESS,
referenceModuleData: [],
})
).to.be.revertedWith(ERRORS.PAUSED);
).to.be.revertedWith(ERRORS.PUBLISHING_PAUSED);
await expect(
lensHub.connect(governance).setState(ProtocolState.Unpaused)
@@ -685,7 +703,7 @@ makeSuiteCleanRoom('Multi-State Hub', function () {
deadline: MAX_UINT256,
},
})
).to.be.revertedWith(ERRORS.PAUSED);
).to.be.revertedWith(ERRORS.PUBLISHING_PAUSED);
await expect(
lensHub.connect(governance).setState(ProtocolState.Unpaused)
@@ -748,7 +766,7 @@ makeSuiteCleanRoom('Multi-State Hub', function () {
referenceModule: ZERO_ADDRESS,
referenceModuleData: [],
})
).to.be.revertedWith(ERRORS.PAUSED);
).to.be.revertedWith(ERRORS.PUBLISHING_PAUSED);
await expect(
lensHub.connect(governance).setState(ProtocolState.Unpaused)
@@ -821,7 +839,7 @@ makeSuiteCleanRoom('Multi-State Hub', function () {
deadline: MAX_UINT256,
},
})
).to.be.revertedWith(ERRORS.PAUSED);
).to.be.revertedWith(ERRORS.PUBLISHING_PAUSED);
await expect(
lensHub.connect(governance).setState(ProtocolState.Unpaused)

View File

@@ -184,6 +184,19 @@ makeSuiteCleanRoom('Profile Creation', function () {
expect(tokenData.mintTimestamp).to.eq(timestamp);
});
it('User should be able to create a profile with a handle including "-" and "_" characters', async function () {
await expect(
lensHub.createProfile({
to: userAddress,
handle: 'morse--__-_--code',
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
});
it('User should be able to create a profile with a handle 16 bytes long, then fail to create with the same handle, and create again with a different handle', async function () {
await expect(
lensHub.createProfile({

View File

@@ -6,9 +6,11 @@ import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
import { ERRORS } from '../../helpers/errors';
import {
cancelWithPermitForAll,
getJsonMetadataFromBase64TokenUri,
getDecodedSvgImage,
getMetadataFromBase64TokenUri,
getSetFollowNFTURIWithSigParts,
getSetProfileImageURIWithSigParts,
loadTestResourceAsUtf8String,
} from '../../helpers/utils';
import {
FIRST_PROFILE_ID,
@@ -67,132 +69,130 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
context('Scenarios', function () {
it('User should have a custom picture tokenURI after setting the profile imageURI', async function () {
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, MOCK_URI)).to.not.be.reverted;
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const tokenUri = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadata = await getMetadataFromBase64TokenUri(tokenUri);
expect(metadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xe1731460db6ce5fa9f3a9f2bd778a8af49e623dceb531df6b1a5c162b7c2d79a'
);
expect(metadata.attributes).to.eql(expectedAttributes);
const actualSvg = await getDecodedSvgImage(metadata);
const expectedSvg = loadTestResourceAsUtf8String('profile-token-uri-images/mock.svg');
expect(actualSvg).to.eq(expectedSvg);
});
it('Default image should be used when no imageURI set', async function () {
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, '')).to.not.be.reverted;
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const tokenUri = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadata = await getMetadataFromBase64TokenUri(tokenUri);
expect(metadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xa21f2a3aa9300a248d3a7acd3f4ff309291653121df87ffe3be545fa1dbd65e5'
);
expect(metadata.attributes).to.eql(expectedAttributes);
const actualSvg = await getDecodedSvgImage(metadata);
const expectedSvg = loadTestResourceAsUtf8String('profile-token-uri-images/default.svg');
expect(actualSvg).to.eq(expectedSvg);
});
it('Default image should be used when imageURI contains double-quotes', async function () {
const imageURI =
const imageUri =
'https://ipfs.io/ipfs/QmbWqxBEKC3P8tqsKc98xmWNzrztRLMiMPL8wBuTGsMnR" <rect x="10" y="10" fill="red';
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, imageURI)).to.not.be.reverted;
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
await expect(lensHub.setProfileImageURI(FIRST_PROFILE_ID, imageUri)).to.not.be.reverted;
const tokenUri = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadata = await getMetadataFromBase64TokenUri(tokenUri);
expect(metadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xa21f2a3aa9300a248d3a7acd3f4ff309291653121df87ffe3be545fa1dbd65e5'
);
expect(metadata.attributes).to.eql(expectedAttributes);
const actualSvg = await getDecodedSvgImage(metadata);
const expectedSvg = loadTestResourceAsUtf8String('profile-token-uri-images/default.svg');
expect(actualSvg).to.eq(expectedSvg);
});
it('Should return the correct tokenURI after transfer', async function () {
const tokenURIBeforeTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataBeforeTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIBeforeTransfer
);
expect(jsonMetadataBeforeTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataBeforeTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const tokenUriBeforeTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadataBeforeTransfer = await getMetadataFromBase64TokenUri(tokenUriBeforeTransfer);
expect(metadataBeforeTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadataBeforeTransfer.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributesBeforeTransfer = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataBeforeTransfer.attributes).to.eql(expectedAttributesBeforeTransfer);
expect(metadataBeforeTransfer.attributes).to.eql(expectedAttributesBeforeTransfer);
const svgBeforeTransfer = await getDecodedSvgImage(metadataBeforeTransfer);
const expectedSvg = loadTestResourceAsUtf8String(
'profile-token-uri-images/mock-profile.svg'
);
expect(svgBeforeTransfer).to.eq(expectedSvg);
await expect(
lensHub.transferFrom(userAddress, userTwoAddress, FIRST_PROFILE_ID)
).to.not.be.reverted;
const tokenURIAfterTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataAfterTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIAfterTransfer
);
expect(jsonMetadataAfterTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataAfterTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const tokenUriAfterTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadataAfterTransfer = await getMetadataFromBase64TokenUri(tokenUriAfterTransfer);
expect(metadataAfterTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadataAfterTransfer.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributesAfterTransfer = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userTwoAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataAfterTransfer.attributes).to.eql(expectedAttributesAfterTransfer);
expect(metadataAfterTransfer.attributes).to.eql(expectedAttributesAfterTransfer);
const svgAfterTransfer = await getDecodedSvgImage(metadataAfterTransfer);
expect(svgAfterTransfer).to.eq(expectedSvg);
});
it('Should return the correct tokenURI after a follow', async function () {
const tokenURIBeforeTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataBeforeTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIBeforeTransfer
);
expect(jsonMetadataBeforeTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataBeforeTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const expectedAttributesBeforeTransfer = [
const tokenUriBeforeFollow = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadataBeforeFollow = await getMetadataFromBase64TokenUri(tokenUriBeforeFollow);
expect(metadataBeforeFollow.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadataBeforeFollow.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributesBeforeFollow = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataBeforeTransfer.attributes).to.eql(expectedAttributesBeforeTransfer);
expect(metadataBeforeFollow.attributes).to.eql(expectedAttributesBeforeFollow);
const svgBeforeFollow = await getDecodedSvgImage(metadataBeforeFollow);
const expectedSvg = loadTestResourceAsUtf8String(
'profile-token-uri-images/mock-profile.svg'
);
expect(svgBeforeFollow).to.eq(expectedSvg);
await expect(lensHub.follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
const tokenURIAfterTransfer = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadataAfterTransfer = await getJsonMetadataFromBase64TokenUri(
tokenURIAfterTransfer
);
expect(jsonMetadataAfterTransfer.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadataAfterTransfer.description).to.eq(
`@${MOCK_PROFILE_HANDLE} - Lens profile`
);
const expectedAttributesAfterTransfer = [
const tokenUriAfterFollow = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadataAfterFollow = await getMetadataFromBase64TokenUri(tokenUriAfterFollow);
expect(metadataAfterFollow.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadataAfterFollow.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributesAfterFollow = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '1' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadataAfterTransfer.attributes).to.eql(expectedAttributesAfterTransfer);
expect(metadataAfterFollow.attributes).to.eql(expectedAttributesAfterFollow);
const svgAfterFollow = await getDecodedSvgImage(metadataAfterFollow);
expect(svgAfterFollow).to.eq(expectedSvg);
});
it('User should set user two as a dispatcher on their profile, user two should set the profile URI', async function () {
@@ -200,10 +200,20 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
await expect(
lensHub.connect(userTwo).setProfileImageURI(FIRST_PROFILE_ID, MOCK_URI)
).to.not.be.reverted;
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xe1731460db6ce5fa9f3a9f2bd778a8af49e623dceb531df6b1a5c162b7c2d79a'
);
const tokenUri = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadata = await getMetadataFromBase64TokenUri(tokenUri);
expect(metadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(metadata.attributes).to.eql(expectedAttributes);
const actualSvg = await getDecodedSvgImage(metadata);
const expectedSvg = loadTestResourceAsUtf8String('profile-token-uri-images/mock.svg');
expect(actualSvg).to.eq(expectedSvg);
});
it('User should follow profile 1, user should change the follow NFT URI, URI is accurate before and after the change', async function () {
@@ -436,11 +446,22 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
MAX_UINT256
);
const tokenURIBefore = await lensHub.tokenURI(FIRST_PROFILE_ID);
expect(keccak256(toUtf8Bytes(tokenURIBefore))).to.eq(
'0x6ed04aa8ab68b7c5201afb2f9655d8fd483559794ce933b7f2282549ca9e3dba'
const tokenUriBefore = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadataBefore = await getMetadataFromBase64TokenUri(tokenUriBefore);
expect(metadataBefore.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadataBefore.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributesBefore = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: testWallet.address.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(metadataBefore.attributes).to.eql(expectedAttributesBefore);
const svgBefore = await getDecodedSvgImage(metadataBefore);
const expectedSvgBefore = loadTestResourceAsUtf8String(
'profile-token-uri-images/mock-profile.svg'
);
expect(svgBefore).to.eq(expectedSvgBefore);
await expect(
lensHub.setProfileImageURIWithSig({
@@ -455,14 +476,24 @@ makeSuiteCleanRoom('Profile URI Functionality', function () {
})
).to.not.be.reverted;
const tokenURIAfter = await lensHub.tokenURI(FIRST_PROFILE_ID);
const tokenUriAfter = await lensHub.tokenURI(FIRST_PROFILE_ID);
expect(MOCK_PROFILE_URI).to.not.eq(MOCK_URI);
expect(tokenURIBefore).to.not.eq(tokenURIAfter);
expect(tokenUriBefore).to.not.eq(tokenUriAfter);
expect(keccak256(toUtf8Bytes(tokenURIAfter))).to.eq(
'0x2f93fe42168b386790c5615061bcd3c1d8aac1976bddca8cf57eb2bc525541ab'
);
const metadataAfter = await getMetadataFromBase64TokenUri(tokenUriAfter);
expect(metadataAfter.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadataAfter.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributesAfter = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: testWallet.address.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(metadataAfter.attributes).to.eql(expectedAttributesAfter);
const svgAfter = await getDecodedSvgImage(metadataAfter);
const expectedSvgAfter = loadTestResourceAsUtf8String('profile-token-uri-images/mock.svg');
expect(svgAfter).to.eq(expectedSvgAfter);
});
it('TestWallet should set the follow NFT URI with sig', async function () {

View File

@@ -2,6 +2,7 @@ import { BigNumber } from '@ethersproject/contracts/node_modules/@ethersproject/
import { parseEther } from '@ethersproject/units';
import '@nomiclabs/hardhat-ethers';
import { expect } from 'chai';
import { ERC20__factory } from '../../../typechain-types';
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
import { ERRORS } from '../../helpers/errors';
import { getTimestamp, matchEvent, waitForTx } from '../../helpers/utils';
@@ -13,6 +14,7 @@ import {
FIRST_PROFILE_ID,
governance,
lensHub,
lensHubImpl,
makeSuiteCleanRoom,
MOCK_FOLLOW_NFT_URI,
MOCK_PROFILE_HANDLE,
@@ -22,6 +24,7 @@ import {
REFERRAL_FEE_BPS,
treasuryAddress,
TREASURY_FEE_BPS,
user,
userAddress,
userTwo,
userTwoAddress,
@@ -102,7 +105,7 @@ makeSuiteCleanRoom('Fee Collect Module', function () {
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
});
it('user should fail to post with fee collect module using amount lower than max BPS', async function () {
it('user should fail to post with fee collect module using zero amount', async function () {
const collectModuleData = abiCoder.encode(
['uint256', 'address', 'address', 'uint16', 'bool'],
[9999, currency.address, userAddress, REFERRAL_FEE_BPS, true]
@@ -138,6 +141,105 @@ makeSuiteCleanRoom('Fee Collect Module', function () {
).to.not.be.reverted;
});
it('Governance should set the treasury fee BPS to zero, userTwo collecting should not emit a transfer event to the treasury', async function () {
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(feeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(1);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, DEFAULT_COLLECT_PRICE],
currency,
currency.address
);
});
it('UserTwo should mirror the original post, governance should set the treasury fee BPS to zero, userTwo collecting their mirror should not emit a transfer event to the treasury', async function () {
const secondProfileId = FIRST_PROFILE_ID + 1;
await expect(
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).mirror({
profileId: secondProfileId,
profileIdPointed: FIRST_PROFILE_ID,
pubIdPointed: 1,
referenceModule: ZERO_ADDRESS,
referenceModuleData: [],
})
).to.not.be.reverted;
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(feeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(secondProfileId, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(2);
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
.mul(REFERRAL_FEE_BPS)
.div(BPS_MAX);
const amount = DEFAULT_COLLECT_PRICE.sub(expectedReferralAmount);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, amount],
currency,
currency.address
);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userTwoAddress, expectedReferralAmount],
currency,
currency.address
);
});
it('UserTwo should fail to collect without following', async function () {
const data = abiCoder.encode(
['address', 'uint256'],

View File

@@ -134,7 +134,7 @@ makeSuiteCleanRoom('Limited Fee Collect Module', function () {
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
});
it('user should fail to post with limited fee collect module using amount lower than max BPS', async function () {
it('user should fail to post with limited fee collect module using zero amount', async function () {
const collectModuleData = abiCoder.encode(
['uint256', 'uint256', 'address', 'address', 'uint16', 'bool'],
[DEFAULT_COLLECT_LIMIT, 9999, currency.address, userAddress, REFERRAL_FEE_BPS, true]
@@ -177,6 +177,105 @@ makeSuiteCleanRoom('Limited Fee Collect Module', function () {
).to.not.be.reverted;
});
it('Governance should set the treasury fee BPS to zero, userTwo collecting should not emit a transfer event to the treasury', async function () {
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(limitedFeeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(1);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, DEFAULT_COLLECT_PRICE],
currency,
currency.address
);
});
it('UserTwo should mirror the original post, governance should set the treasury fee BPS to zero, userTwo collecting their mirror should not emit a transfer event to the treasury', async function () {
const secondProfileId = FIRST_PROFILE_ID + 1;
await expect(
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).mirror({
profileId: secondProfileId,
profileIdPointed: FIRST_PROFILE_ID,
pubIdPointed: 1,
referenceModule: ZERO_ADDRESS,
referenceModuleData: [],
})
).to.not.be.reverted;
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(limitedFeeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(secondProfileId, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(2);
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
.mul(REFERRAL_FEE_BPS)
.div(BPS_MAX);
const amount = DEFAULT_COLLECT_PRICE.sub(expectedReferralAmount);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, amount],
currency,
currency.address
);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userTwoAddress, expectedReferralAmount],
currency,
currency.address
);
});
it('UserTwo should fail to collect without following', async function () {
const data = abiCoder.encode(
['address', 'uint256'],

View File

@@ -134,7 +134,7 @@ makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
});
it('user should fail to post with limited timed fee collect module using amount lower than max BPS', async function () {
it('user should fail to post with limited timed fee collect module using zero amount', async function () {
const collectModuleData = abiCoder.encode(
['uint256', 'uint256', 'address', 'address', 'uint16', 'bool'],
[DEFAULT_COLLECT_LIMIT, 9999, currency.address, userAddress, REFERRAL_FEE_BPS, true]
@@ -177,6 +177,105 @@ makeSuiteCleanRoom('Limited Timed Fee Collect Module', function () {
).to.not.be.reverted;
});
it('Governance should set the treasury fee BPS to zero, userTwo collecting should not emit a transfer event to the treasury', async function () {
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(limitedTimedFeeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(1);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, DEFAULT_COLLECT_PRICE],
currency,
currency.address
);
});
it('UserTwo should mirror the original post, governance should set the treasury fee BPS to zero, userTwo collecting their mirror should not emit a transfer event to the treasury', async function () {
const secondProfileId = FIRST_PROFILE_ID + 1;
await expect(
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).mirror({
profileId: secondProfileId,
profileIdPointed: FIRST_PROFILE_ID,
pubIdPointed: 1,
referenceModule: ZERO_ADDRESS,
referenceModuleData: [],
})
).to.not.be.reverted;
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(limitedTimedFeeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(secondProfileId, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(2);
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
.mul(REFERRAL_FEE_BPS)
.div(BPS_MAX);
const amount = DEFAULT_COLLECT_PRICE.sub(expectedReferralAmount);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, amount],
currency,
currency.address
);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userTwoAddress, expectedReferralAmount],
currency,
currency.address
);
});
it('UserTwo should fail to collect without following', async function () {
const data = abiCoder.encode(
['address', 'uint256'],

View File

@@ -102,7 +102,7 @@ makeSuiteCleanRoom('Timed Fee Collect Module', function () {
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
});
it('user should fail to post with timed fee collect module using amount lower than max BPS', async function () {
it('user should fail to post with timed fee collect module using zero amount', async function () {
const collectModuleData = abiCoder.encode(
['uint256', 'address', 'address', 'uint16', 'bool'],
[9999, currency.address, userAddress, REFERRAL_FEE_BPS, true]
@@ -138,6 +138,105 @@ makeSuiteCleanRoom('Timed Fee Collect Module', function () {
).to.not.be.reverted;
});
it('Governance should set the treasury fee BPS to zero, userTwo collecting should not emit a transfer event to the treasury', async function () {
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(timedFeeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(FIRST_PROFILE_ID, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(1);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, DEFAULT_COLLECT_PRICE],
currency,
currency.address
);
});
it('UserTwo should mirror the original post, governance should set the treasury fee BPS to zero, userTwo collecting their mirror should not emit a transfer event to the treasury', async function () {
const secondProfileId = FIRST_PROFILE_ID + 1;
await expect(
lensHub.connect(userTwo).createProfile({
to: userTwoAddress,
handle: 'usertwo',
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(
lensHub.connect(userTwo).mirror({
profileId: secondProfileId,
profileIdPointed: FIRST_PROFILE_ID,
pubIdPointed: 1,
referenceModule: ZERO_ADDRESS,
referenceModuleData: [],
})
).to.not.be.reverted;
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_COLLECT_PRICE]
);
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(timedFeeCollectModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).collect(secondProfileId, 1, data);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(2);
const expectedReferralAmount = BigNumber.from(DEFAULT_COLLECT_PRICE)
.mul(REFERRAL_FEE_BPS)
.div(BPS_MAX);
const amount = DEFAULT_COLLECT_PRICE.sub(expectedReferralAmount);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, amount],
currency,
currency.address
);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userTwoAddress, expectedReferralAmount],
currency,
currency.address
);
});
it('UserTwo should fail to collect without following', async function () {
const data = abiCoder.encode(
['address', 'uint256'],

View File

@@ -76,10 +76,10 @@ makeSuiteCleanRoom('Fee Follow Module', function () {
).to.be.revertedWith(ERRORS.INIT_PARAMS_INVALID);
});
it('user should fail to create a profile with fee follow module using amount lower than max BPS', async function () {
it('user should fail to create a profile with fee follow module using zero amount', async function () {
const followModuleData = abiCoder.encode(
['uint256', 'address', 'address'],
[9999, currency.address, userAddress]
[0, currency.address, userAddress]
);
await expect(
@@ -113,6 +113,36 @@ makeSuiteCleanRoom('Fee Follow Module', function () {
).to.not.be.reverted;
});
it('Governance should set the treasury fee BPS to zero, userTwo following should not emit a transfer event to the treasury', async function () {
await expect(moduleGlobals.connect(governance).setTreasuryFee(0)).to.not.be.reverted;
const data = abiCoder.encode(
['address', 'uint256'],
[currency.address, DEFAULT_FOLLOW_PRICE]
);
await expect(currency.mint(userTwoAddress, MAX_UINT256)).to.not.be.reverted;
await expect(
currency.connect(userTwo).approve(feeFollowModule.address, MAX_UINT256)
).to.not.be.reverted;
const tx = lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [data]);
const receipt = await waitForTx(tx);
let currencyEventCount = 0;
for (let log of receipt.logs) {
if (log.address == currency.address) {
currencyEventCount++;
}
}
expect(currencyEventCount).to.eq(1);
matchEvent(
receipt,
'Transfer',
[userTwoAddress, userAddress, DEFAULT_FOLLOW_PRICE],
currency,
currency.address
);
});
it('UserTwo should fail to follow passing a different expected price in data', async function () {
const data = abiCoder.encode(
['address', 'uint256'],

View File

@@ -4,7 +4,11 @@ import { keccak256, toUtf8Bytes } from 'ethers/lib/utils';
import { UIDataProvider__factory } from '../../typechain-types';
import { ZERO_ADDRESS } from '../helpers/constants';
import { ERRORS } from '../helpers/errors';
import { getJsonMetadataFromBase64TokenUri } from '../helpers/utils';
import {
getDecodedSvgImage,
getMetadataFromBase64TokenUri,
loadTestResourceAsUtf8String,
} from '../helpers/utils';
import {
approvalFollowModule,
deployer,
@@ -156,20 +160,20 @@ makeSuiteCleanRoom('Misc', function () {
});
it('Profile tokenURI should return the accurate URI', async function () {
const tokenURI = await lensHub.tokenURI(FIRST_PROFILE_ID);
const jsonMetadata = await getJsonMetadataFromBase64TokenUri(tokenURI);
expect(jsonMetadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(jsonMetadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const tokenUri = await lensHub.tokenURI(FIRST_PROFILE_ID);
const metadata = await getMetadataFromBase64TokenUri(tokenUri);
expect(metadata.name).to.eq(`@${MOCK_PROFILE_HANDLE}`);
expect(metadata.description).to.eq(`@${MOCK_PROFILE_HANDLE} - Lens profile`);
const expectedAttributes = [
{ trait_type: 'id', value: `#${FIRST_PROFILE_ID.toString()}` },
{ trait_type: 'followers', value: '0' },
{ trait_type: 'owner', value: userAddress.toLowerCase() },
{ trait_type: 'handle', value: `@${MOCK_PROFILE_HANDLE}` },
];
expect(jsonMetadata.attributes).to.eql(expectedAttributes);
expect(keccak256(toUtf8Bytes(tokenURI))).to.eq(
'0xa2e9967e825705ce8f38931f7d1f88fe63bef6f2f2c52715692f14d42d889b76'
);
expect(metadata.attributes).to.eql(expectedAttributes);
const actualSvg = await getDecodedSvgImage(metadata);
const expectedSvg = loadTestResourceAsUtf8String('profile-token-uri-images/mock-profile.svg');
expect(actualSvg).to.eq(expectedSvg);
});
it('Publication reference module getter should return the correct reference module (or zero in case of no reference module)', async function () {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB