Merge branch 'main' of https://github.com/aave/lens-protocol into feature/proposal-to-allow-follow-after-transfer

This commit is contained in:
damarnez
2022-03-02 21:44:20 +01:00
8 changed files with 536 additions and 14 deletions

22
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
compile_and_run_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Compile code
run: npm run compile
- name: Run tests
run: npm run test

View File

@@ -156,6 +156,43 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
);
}
/// @inheritdoc ILensHub
function setDefaultProfile(uint256 profileId, address wallet) external override whenNotPaused {
_validateCallerIsProfileOwnerOrDispatcher(profileId);
_setDefaultProfile(profileId, wallet);
}
/// @inheritdoc ILensHub
function setDefaultProfileWithSig(DataTypes.SetDefaultProfileWithSigData calldata vars)
external
override
whenNotPaused
{
address owner = ownerOf(vars.profileId);
bytes32 digest;
unchecked {
digest = keccak256(
abi.encodePacked(
'\x19\x01',
_calculateDomainSeparator(),
keccak256(
abi.encode(
SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH,
vars.profileId,
vars.wallet,
sigNonces[owner]++,
vars.sig.deadline
)
)
)
);
}
_validateRecoveredAddress(digest, owner, vars.sig);
_setDefaultProfile(vars.profileId, vars.wallet);
}
/// @inheritdoc ILensHub
function setFollowModule(
uint256 profileId,
@@ -719,6 +756,11 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
return _profileCreatorWhitelisted[profileCreator];
}
/// @inheritdoc ILensHub
function defaultProfile(address wallet) external view override returns (uint256) {
return _defaultProfileByAddress[wallet];
}
/// @inheritdoc ILensHub
function isFollowModuleWhitelisted(address followModule) external view override returns (bool) {
return _followModuleWhitelisted[followModule];
@@ -921,6 +963,23 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
);
}
function _setDefaultProfile(uint256 profileId, address wallet) internal {
// you should only be able to map this to the owner OR dead address
if (wallet != address(0)) {
_validateWalletIsProfileOwner(profileId, wallet);
_defaultProfileByAddress[wallet] = profileId;
_addressByDefaultProfile[profileId] = wallet;
emit Events.DefaultProfileSet(profileId, wallet, block.timestamp);
} else {
// unset the default
_defaultProfileByAddress[ownerOf(profileId)] = 0;
_addressByDefaultProfile[profileId] = wallet;
emit Events.DefaultProfileSet(0, wallet, block.timestamp);
}
}
function _createComment(DataTypes.CommentData memory vars) internal {
PublishingLogic.createComment(
vars,
@@ -980,6 +1039,12 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
if (_dispatcherByProfile[tokenId] != address(0)) {
_setDispatcher(tokenId, address(0));
}
if (from != address(0)) {
_addressByDefaultProfile[tokenId] = address(0);
_defaultProfileByAddress[from] = 0;
}
super._beforeTokenTransfer(from, to, tokenId);
}
@@ -992,6 +1057,10 @@ contract LensHub is ILensHub, LensNFTBase, VersionedInitializable, LensMultiStat
if (msg.sender != ownerOf(profileId)) revert Errors.NotProfileOwner();
}
function _validateWalletIsProfileOwner(uint256 profileId, address wallet) internal view {
if (wallet != ownerOf(profileId)) revert Errors.NotProfileOwner();
}
function _validateCallerIsGovernance() internal view {
if (msg.sender != _governance) revert Errors.NotGovernance();
}

View File

@@ -10,6 +10,11 @@ contract LensHubStorage {
// keccak256(
// 'CreateProfileWithSig(string handle,string uri,address followModule,bytes followModuleData,uint256 nonce,uint256 deadline)'
// );
bytes32 internal constant SET_DEFAULT_PROFILE_WITH_SIG_TYPEHASH =
0xae4d0f1a57c80ed196993d814b14f19b25a688b09b9cb0467c33d76e022c216f;
// keccak256(
// 'SetDefaultProfileWithSig(uint256 profileId,address wallet,uint256 nonce,uint256 deadline)'
// );
bytes32 internal constant SET_FOLLOW_MODULE_WITH_SIG_TYPEHASH =
0x6f3f6455a608af1cc57ef3e5c0a49deeb88bba264ec8865b798ff07358859d4b;
// keccak256(
@@ -71,6 +76,9 @@ contract LensHubStorage {
mapping(uint256 => DataTypes.ProfileStruct) internal _profileById;
mapping(uint256 => mapping(uint256 => DataTypes.PublicationStruct)) internal _pubByIdByProfile;
mapping(uint256 => address) internal _addressByDefaultProfile;
mapping(address => uint256) internal _defaultProfileByAddress;
uint256 internal _profileCounter;
address internal _governance;
address internal _emergencyAdmin;

View File

@@ -99,6 +99,22 @@ interface ILensHub {
*/
function createProfile(DataTypes.CreateProfileData calldata vars) external;
/**
* @notice Sets the mapping between wallet and its main profile identity
*
* @param profileId The token ID of the profile to set as the main profile identity
* @param wallet The address of the wallet which is either the owner of the profile or address(0)
*/
function setDefaultProfile(uint256 profileId, address wallet) external;
/**
* @notice Sets the mapping between wallet and its main profile identity via signature with the specified parameters.
*
* @param vars A SetDefaultProfileWithSigData struct, including the regular parameters and an EIP712Signature struct.
*/
function setDefaultProfileWithSig(DataTypes.SetDefaultProfileWithSigData calldata vars)
external;
/**
* @notice Sets a profile's follow module, must be called by the profile owner.
*
@@ -317,6 +333,15 @@ interface ILensHub {
*/
function isProfileCreatorWhitelisted(address profileCreator) external view returns (bool);
/**
* @notice Returns default profile for a given wallet address
*
* @param wallet The address to find the default mapping
*
* @return A uint256 profile id will be 0 if not mapped
*/
function defaultProfile(address wallet) external view returns (uint256);
/**
* @notice Returns whether or not a follow module is whitelisted.
*

View File

@@ -109,6 +109,20 @@ library DataTypes {
string followNFTURI;
}
/**
* @notice A struct containing the parameters required for the `setDefaultProfileWithSig()` function. Parameters are
* the same as the regular `setDefaultProfile()` function, with an added EIP712Signature.
*
* @param profileId The token ID of the profile which will be set as default
* @param wallet The address of the wallet which is either the owner of the profile or address(0)
* @param sig The EIP712Signature struct containing the profile owner's signature.
*/
struct SetDefaultProfileWithSigData {
uint256 profileId;
address wallet;
EIP712Signature sig;
}
/**
* @notice A struct containing the parameters required for the `setFollowModuleWithSig()` function. Parameters are
* the same as the regular `setFollowModule()` function, with an added EIP712Signature.

View File

@@ -138,6 +138,15 @@ library Events {
uint256 timestamp
);
/**
* @dev Emitted when a a default profile is set for a wallet as its main identity
*
* @param profileId The token ID of the profile for which the default profile is being set.
* @param wallet The wallet which owns this profile
* @param timestamp The current block timestamp.
*/
event DefaultProfileSet(uint256 indexed profileId, address indexed wallet, uint256 timestamp);
/**
* @dev Emitted when a dispatcher is set for a specific profile.
*

View File

@@ -1,22 +1,12 @@
import { TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
import '@nomiclabs/hardhat-ethers';
import {
BigNumberish,
Bytes,
Event,
logger,
utils,
BigNumber,
Contract,
ContractReceipt,
} from 'ethers';
import { TransactionReceipt } from '@ethersproject/providers';
import { expect } from 'chai';
import { BigNumber, BigNumberish, Bytes, Contract, logger, utils } from 'ethers';
import { hexlify, keccak256, RLP, toUtf8Bytes } from 'ethers/lib/utils';
import { TransactionResponse } from '@ethersproject/providers';
import hre from 'hardhat';
import { LensHub__factory } from '../../typechain-types';
import { lensHub, LENS_HUB_NFT_NAME, helper, testWallet, eventsLib } from '../__setup.spec';
import { eventsLib, helper, lensHub, LENS_HUB_NFT_NAME, testWallet } from '../__setup.spec';
import { HARDHAT_CHAINID, MAX_UINT256 } from './constants';
import { expect } from 'chai';
export enum ProtocolState {
Unpaused,
@@ -327,6 +317,16 @@ export async function getSetProfileImageURIWithSigParts(
return await getSig(msgParams);
}
export async function getSetDefaultProfileWithSigParts(
profileId: BigNumberish,
wallet: string,
nonce: number,
deadline: string
): Promise<{ v: number; r: string; s: string }> {
const msgParams = buildSetDefaultProfileWithSigParams(profileId, wallet, nonce, deadline);
return await getSig(msgParams);
}
export async function getSetFollowNFTURIWithSigParts(
profileId: BigNumberish,
followNFTURI: string,
@@ -592,6 +592,29 @@ const buildSetProfileImageURIWithSigParams = (
},
});
const buildSetDefaultProfileWithSigParams = (
profileId: BigNumberish,
wallet: string,
nonce: number,
deadline: string
) => ({
types: {
SetDefaultProfileWithSig: [
{ name: 'profileId', type: 'uint256' },
{ name: 'wallet', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
domain: domain(),
value: {
profileId: profileId,
wallet: wallet,
nonce: nonce,
deadline: deadline,
},
});
const buildSetFollowNFTURIWithSigParams = (
profileId: BigNumberish,
followNFTURI: string,

View File

@@ -0,0 +1,352 @@
import '@nomiclabs/hardhat-ethers';
import { expect } from 'chai';
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
import { ERRORS } from '../../helpers/errors';
import { cancelWithPermitForAll, getSetDefaultProfileWithSigParts } from '../../helpers/utils';
import {
FIRST_PROFILE_ID,
lensHub,
makeSuiteCleanRoom,
MOCK_FOLLOW_NFT_URI,
MOCK_PROFILE_HANDLE,
MOCK_PROFILE_URI,
testWallet,
userAddress,
userTwo,
userTwoAddress,
} from '../../__setup.spec';
makeSuiteCleanRoom('Default profile Functionality', function () {
context('Generic', function () {
beforeEach(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;
});
context('Negatives', function () {
it('UserTwo should fail to set the default profile on profile owned by user 1', async function () {
await expect(
lensHub.connect(userTwo).setDefaultProfile(FIRST_PROFILE_ID, userTwoAddress)
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER_OR_DISPATCHER);
});
it('UserOne should fail to change the default profile for address that doesnt own the profile', async function () {
await expect(
lensHub.setDefaultProfile(FIRST_PROFILE_ID, userTwoAddress)
).to.be.revertedWith(ERRORS.NOT_PROFILE_OWNER);
});
});
context('Scenarios', function () {
it('User should set the default profile', async function () {
await expect(lensHub.setDefaultProfile(FIRST_PROFILE_ID, userAddress)).to.not.be.reverted;
expect((await lensHub.defaultProfile(userAddress)).toNumber()).to.eq(FIRST_PROFILE_ID);
});
it('User should set the default profile and then be able to unset it', async function () {
await expect(lensHub.setDefaultProfile(FIRST_PROFILE_ID, userAddress)).to.not.be.reverted;
expect((await lensHub.defaultProfile(userAddress)).toNumber()).to.eq(FIRST_PROFILE_ID);
await expect(lensHub.setDefaultProfile(FIRST_PROFILE_ID, ZERO_ADDRESS)).to.not.be.reverted;
expect((await lensHub.defaultProfile(userAddress)).toNumber()).to.eq(0);
});
it('User should set the default profile and then be able to change it to another', async function () {
await expect(lensHub.setDefaultProfile(FIRST_PROFILE_ID, userAddress)).to.not.be.reverted;
expect((await lensHub.defaultProfile(userAddress)).toNumber()).to.eq(FIRST_PROFILE_ID);
await expect(
lensHub.createProfile({
to: userAddress,
handle: new Date().getTime().toString(),
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
await expect(lensHub.setDefaultProfile(2, userAddress)).to.not.be.reverted;
expect((await lensHub.defaultProfile(userAddress)).toNumber()).to.eq(2);
});
});
});
context('Meta-tx', function () {
beforeEach(async function () {
await expect(
lensHub.connect(testWallet).createProfile({
to: testWallet.address,
handle: MOCK_PROFILE_HANDLE,
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
});
context('Negatives', function () {
it('TestWallet should fail to set default profile with sig with signature deadline mismatch', async function () {
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
const { v, r, s } = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
testWallet.address,
nonce,
'0'
);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: testWallet.address,
sig: {
v,
r,
s,
deadline: MAX_UINT256,
},
})
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
});
it('TestWallet should fail to set default profile with sig with invalid deadline', async function () {
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
const { v, r, s } = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
testWallet.address,
nonce,
'0'
);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: testWallet.address,
sig: {
v,
r,
s,
deadline: '0',
},
})
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
});
it('TestWallet should fail to set default profile with sig with invalid nonce', async function () {
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
const { v, r, s } = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
testWallet.address,
nonce + 1,
MAX_UINT256
);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: testWallet.address,
sig: {
v,
r,
s,
deadline: MAX_UINT256,
},
})
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
});
it('TestWallet should sign attempt to set default profile with sig, cancel with empty permitForAll, then fail to set default profile with sig', async function () {
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
const { v, r, s } = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
testWallet.address,
nonce,
MAX_UINT256
);
await cancelWithPermitForAll();
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: testWallet.address,
sig: {
v,
r,
s,
deadline: MAX_UINT256,
},
})
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
});
});
context('Scenarios', function () {
it('TestWallet should set the default profile with sig', async function () {
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
const { v, r, s } = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
testWallet.address,
nonce,
MAX_UINT256
);
const defaultProfileBeforeUse = await lensHub.defaultProfile(testWallet.address);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: testWallet.address,
sig: {
v,
r,
s,
deadline: MAX_UINT256,
},
})
).to.not.be.reverted;
const defaultProfileAfter = await lensHub.defaultProfile(testWallet.address);
expect(defaultProfileBeforeUse.toNumber()).to.eq(0);
expect(defaultProfileAfter.toNumber()).to.eq(FIRST_PROFILE_ID);
});
it('TestWallet should set the default profile with sig and then be able to unset it', async function () {
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
const { v, r, s } = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
testWallet.address,
nonce,
MAX_UINT256
);
const defaultProfileBeforeUse = await lensHub.defaultProfile(testWallet.address);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: testWallet.address,
sig: {
v,
r,
s,
deadline: MAX_UINT256,
},
})
).to.not.be.reverted;
const defaultProfileAfter = await lensHub.defaultProfile(testWallet.address);
expect(defaultProfileBeforeUse.toNumber()).to.eq(0);
expect(defaultProfileAfter.toNumber()).to.eq(FIRST_PROFILE_ID);
const nonce2 = (await lensHub.sigNonces(testWallet.address)).toNumber();
const signature2 = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
ZERO_ADDRESS,
nonce2,
MAX_UINT256
);
const defaultProfileBeforeUse2 = await lensHub.defaultProfile(testWallet.address);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: ZERO_ADDRESS,
sig: {
v: signature2.v,
r: signature2.r,
s: signature2.s,
deadline: MAX_UINT256,
},
})
).to.not.be.reverted;
const defaultProfileAfter2 = await lensHub.defaultProfile(testWallet.address);
expect(defaultProfileBeforeUse2.toNumber()).to.eq(1);
expect(defaultProfileAfter2.toNumber()).to.eq(0);
});
it('TestWallet should set the default profile and then be able to change it to another', async function () {
const nonce = (await lensHub.sigNonces(testWallet.address)).toNumber();
const { v, r, s } = await getSetDefaultProfileWithSigParts(
FIRST_PROFILE_ID,
testWallet.address,
nonce,
MAX_UINT256
);
const defaultProfileBeforeUse = await lensHub.defaultProfile(testWallet.address);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: FIRST_PROFILE_ID,
wallet: testWallet.address,
sig: {
v,
r,
s,
deadline: MAX_UINT256,
},
})
).to.not.be.reverted;
const defaultProfileAfter = await lensHub.defaultProfile(testWallet.address);
expect(defaultProfileBeforeUse.toNumber()).to.eq(0);
expect(defaultProfileAfter.toNumber()).to.eq(FIRST_PROFILE_ID);
await expect(
lensHub.createProfile({
to: testWallet.address,
handle: new Date().getTime().toString(),
imageURI: MOCK_PROFILE_URI,
followModule: ZERO_ADDRESS,
followModuleData: [],
followNFTURI: MOCK_FOLLOW_NFT_URI,
})
).to.not.be.reverted;
const nonce2 = (await lensHub.sigNonces(testWallet.address)).toNumber();
const signature2 = await getSetDefaultProfileWithSigParts(
2,
testWallet.address,
nonce2,
MAX_UINT256
);
const defaultProfileBeforeUse2 = await lensHub.defaultProfile(testWallet.address);
await expect(
lensHub.setDefaultProfileWithSig({
profileId: 2,
wallet: testWallet.address,
sig: {
v: signature2.v,
r: signature2.r,
s: signature2.s,
deadline: MAX_UINT256,
},
})
).to.not.be.reverted;
const defaultProfileAfter2 = await lensHub.defaultProfile(testWallet.address);
expect(defaultProfileBeforeUse2.toNumber()).to.eq(1);
expect(defaultProfileAfter2.toNumber()).to.eq(2);
});
});
});
});