Skip to content

Commit df2a15f

Browse files
Merge pull request #124 from aave/feat/main-profile-proxy
Feat: Add A Main Profile Creation Proxy
2 parents 14bc495 + ab442ea commit df2a15f

File tree

7 files changed

+229
-5
lines changed

7 files changed

+229
-5
lines changed

contracts/libraries/Errors.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ library Errors {
2626
error HandleTaken();
2727
error HandleLengthInvalid();
2828
error HandleContainsInvalidCharacters();
29+
error HandleFirstCharInvalid();
2930
error ProfileImageURILengthInvalid();
3031
error CallerNotFollowNFT();
3132
error CallerNotCollectNFT();
3233
error BlockNumberInvalid();
3334
error ArrayMismatch();
3435
error CannotCommentOnSelf();
36+
error NotWhitelisted();
3537

3638
// Module Errors
3739
error InitParamsInvalid();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.8.10;
4+
5+
import {ILensHub} from '../interfaces/ILensHub.sol';
6+
import {DataTypes} from '../libraries/DataTypes.sol';
7+
import {Errors} from '../libraries/Errors.sol';
8+
import {Events} from '../libraries/Events.sol';
9+
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
10+
11+
/**
12+
* @title ProfileCreationProxy
13+
* @author Lens Protocol
14+
*
15+
* @notice This is an ownable proxy contract that enforces ".lens" handle suffixes at profile creation.
16+
* Only the owner can create profiles.
17+
*/
18+
contract ProfileCreationProxy is Ownable {
19+
ILensHub immutable LENS_HUB;
20+
21+
constructor(address owner, ILensHub hub) {
22+
_transferOwnership(owner);
23+
LENS_HUB = hub;
24+
}
25+
26+
function proxyCreateProfile(DataTypes.CreateProfileData memory vars) external onlyOwner {
27+
uint256 handleLength = bytes(vars.handle).length;
28+
if (handleLength < 5) revert Errors.HandleLengthInvalid();
29+
30+
bytes1 firstByte = bytes(vars.handle)[0];
31+
if (firstByte == '-' || firstByte == '_' || firstByte == '.')
32+
revert Errors.HandleFirstCharInvalid();
33+
34+
for (uint256 i = 1; i < handleLength; ) {
35+
if (bytes(vars.handle)[i] == '.') revert Errors.HandleContainsInvalidCharacters();
36+
unchecked {
37+
++i;
38+
}
39+
}
40+
41+
vars.handle = string(abi.encodePacked(vars.handle, '.lens'));
42+
LENS_HUB.createProfile(vars);
43+
}
44+
}

tasks/full-deploy-verify.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
UIDataProvider__factory,
2626
ProfileFollowModule__factory,
2727
RevertFollowModule__factory,
28+
ProfileCreationProxy__factory,
2829
} from '../typechain-types';
2930
import { deployWithVerify, waitForTx } from './helpers/utils';
3031

@@ -56,11 +57,13 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
5657
const deployer = accounts[0];
5758
const governance = accounts[1];
5859
const treasuryAddress = accounts[2].address;
60+
const proxyAdminAddress = deployer.address;
61+
const profileCreatorAddress = deployer.address;
5962

6063
// Nonce management in case of deployment issues
6164
let deployerNonce = await ethers.provider.getTransactionCount(deployer.address);
6265

63-
console.log('\n\t -- Deploying Module Globals --');
66+
console.log('\n\t-- Deploying Module Globals --');
6467
const moduleGlobals = await deployWithVerify(
6568
new ModuleGlobals__factory(deployer).deploy(
6669
governance.address,
@@ -143,7 +146,7 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
143146
let proxy = await deployWithVerify(
144147
new TransparentUpgradeableProxy__factory(deployer).deploy(
145148
lensHubImpl.address,
146-
deployer.address,
149+
proxyAdminAddress,
147150
data,
148151
{ nonce: deployerNonce++ }
149152
),
@@ -271,6 +274,15 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
271274
'contracts/misc/UIDataProvider.sol:UIDataProvider'
272275
);
273276

277+
console.log('\n\t-- Deploying Profile Creation Proxy --');
278+
const profileCreationProxy = await deployWithVerify(
279+
new ProfileCreationProxy__factory(deployer).deploy(profileCreatorAddress, lensHub.address, {
280+
nonce: deployerNonce++,
281+
}),
282+
[deployer.address, lensHub.address],
283+
'contracts/misc/ProfileCreationProxy.sol:ProfileCreationProxy'
284+
);
285+
274286
// Whitelist the collect modules
275287
console.log('\n\t-- Whitelisting Collect Modules --');
276288
let governanceNonce = await ethers.provider.getTransactionCount(governance.address);
@@ -327,6 +339,14 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
327339
})
328340
);
329341

342+
// Whitelist the profile creation proxy
343+
console.log('\n\t-- Whitelisting Profile Creation Proxy --');
344+
await waitForTx(
345+
lensHub.whitelistProfileCreator(profileCreationProxy.address, true, {
346+
nonce: governanceNonce++,
347+
})
348+
);
349+
330350
// Save and log the addresses
331351
const addrs = {
332352
'lensHub proxy': lensHub.address,
@@ -351,6 +371,7 @@ task('full-deploy-verify', 'deploys the entire Lens Protocol with explorer verif
351371
// 'approval follow module': approvalFollowModule.address,
352372
'follower only reference module': followerOnlyReferenceModule.address,
353373
'UI data provider': uiDataProvider.address,
374+
'Profile creation proxy': profileCreationProxy.address,
354375
};
355376
const json = JSON.stringify(addrs, null, 2);
356377
console.log(json);

tasks/full-deploy.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
UIDataProvider__factory,
2626
ProfileFollowModule__factory,
2727
RevertFollowModule__factory,
28+
ProfileCreationProxy__factory,
2829
} from '../typechain-types';
2930
import { deployContract, waitForTx } from './helpers/utils';
3031

@@ -40,11 +41,13 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
4041
const deployer = accounts[0];
4142
const governance = accounts[1];
4243
const treasuryAddress = accounts[2].address;
44+
const proxyAdminAddress = deployer.address;
45+
const profileCreatorAddress = deployer.address;
4346

4447
// Nonce management in case of deployment issues
4548
let deployerNonce = await ethers.provider.getTransactionCount(deployer.address);
4649

47-
console.log('\n\t -- Deploying Module Globals --');
50+
console.log('\n\t-- Deploying Module Globals --');
4851
const moduleGlobals = await deployContract(
4952
new ModuleGlobals__factory(deployer).deploy(
5053
governance.address,
@@ -113,7 +116,7 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
113116
let proxy = await deployContract(
114117
new TransparentUpgradeableProxy__factory(deployer).deploy(
115118
lensHubImpl.address,
116-
deployer.address,
119+
proxyAdminAddress,
117120
data,
118121
{ nonce: deployerNonce++ }
119122
)
@@ -211,6 +214,13 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
211214
})
212215
);
213216

217+
console.log('\n\t-- Deploying Profile Creation Proxy --');
218+
const profileCreationProxy = await deployContract(
219+
new ProfileCreationProxy__factory(deployer).deploy(profileCreatorAddress, lensHub.address, {
220+
nonce: deployerNonce++,
221+
})
222+
);
223+
214224
// Whitelist the collect modules
215225
console.log('\n\t-- Whitelisting Collect Modules --');
216226
let governanceNonce = await ethers.provider.getTransactionCount(governance.address);
@@ -271,6 +281,14 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
271281
.whitelistCurrency(currency.address, true, { nonce: governanceNonce++ })
272282
);
273283

284+
// Whitelist the profile creation proxy
285+
console.log('\n\t-- Whitelisting Profile Creation Proxy --');
286+
await waitForTx(
287+
lensHub.whitelistProfileCreator(profileCreationProxy.address, true, {
288+
nonce: governanceNonce++,
289+
})
290+
);
291+
274292
// Save and log the addresses
275293
const addrs = {
276294
'lensHub proxy': lensHub.address,
@@ -295,6 +313,7 @@ task('full-deploy', 'deploys the entire Lens Protocol').setAction(async ({}, hre
295313
// 'approval follow module': approvalFollowModule.address,
296314
'follower only reference module': followerOnlyReferenceModule.address,
297315
'UI data provider': uiDataProvider.address,
316+
'Profile creation proxy': profileCreationProxy.address,
298317
};
299318
const json = JSON.stringify(addrs, null, 2);
300319
console.log(json);

tasks/testnet-full-deploy-verify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ task(
6262
// Nonce management in case of deployment issues
6363
let deployerNonce = await ethers.provider.getTransactionCount(deployer.address);
6464

65-
console.log('\n\t -- Deploying Module Globals --');
65+
console.log('\n\t-- Deploying Module Globals --');
6666
const moduleGlobals = await deployWithVerify(
6767
new ModuleGlobals__factory(deployer).deploy(
6868
governance.address,

test/helpers/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const ERRORS = {
2222
INVALID_HANDLE_LENGTH: 'HandleLengthInvalid()',
2323
INVALID_IMAGE_URI_LENGTH: 'ProfileImageURILengthInvalid()',
2424
HANDLE_CONTAINS_INVALID_CHARACTERS: 'HandleContainsInvalidCharacters()',
25+
HANDLE_FIRST_CHARACTER_INVALID: 'HandleFirstCharInvalid()',
2526
NOT_FOLLOW_NFT: 'CallerNotFollowNFT()',
2627
NOT_COLLECT_NFT: 'CallerNotCollectNFT()',
2728
BLOCK_NUMBER_INVALID: 'BlockNumberInvalid()',
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import '@nomiclabs/hardhat-ethers';
2+
import { expect } from 'chai';
3+
import { ZERO_ADDRESS } from '../helpers/constants';
4+
import { ERRORS } from '../helpers/errors';
5+
import { ProfileCreationProxy, ProfileCreationProxy__factory } from '../../typechain-types';
6+
import {
7+
deployer,
8+
FIRST_PROFILE_ID,
9+
governance,
10+
lensHub,
11+
makeSuiteCleanRoom,
12+
MOCK_FOLLOW_NFT_URI,
13+
MOCK_PROFILE_URI,
14+
user,
15+
userAddress,
16+
deployerAddress,
17+
} from '../__setup.spec';
18+
import { BigNumber } from 'ethers';
19+
import { TokenDataStructOutput } from '../../typechain-types/LensHub';
20+
import { getTimestamp } from '../helpers/utils';
21+
22+
makeSuiteCleanRoom('Profile Creation Proxy', function () {
23+
const REQUIRED_SUFFIX = '.lens';
24+
const MINIMUM_LENGTH = 5;
25+
26+
let profileCreationProxy: ProfileCreationProxy;
27+
beforeEach(async function () {
28+
profileCreationProxy = await new ProfileCreationProxy__factory(deployer).deploy(
29+
deployerAddress,
30+
lensHub.address
31+
);
32+
await expect(
33+
lensHub.connect(governance).whitelistProfileCreator(profileCreationProxy.address, true)
34+
).to.not.be.reverted;
35+
});
36+
37+
context('Negatives', function () {
38+
it('Should fail to create profile if handle length before suffix does not reach minimum length', async function () {
39+
const handle = 'a'.repeat(MINIMUM_LENGTH - 1);
40+
await expect(
41+
profileCreationProxy.proxyCreateProfile({
42+
to: userAddress,
43+
handle: handle,
44+
imageURI: MOCK_PROFILE_URI,
45+
followModule: ZERO_ADDRESS,
46+
followModuleInitData: [],
47+
followNFTURI: MOCK_FOLLOW_NFT_URI,
48+
})
49+
).to.be.revertedWith(ERRORS.INVALID_HANDLE_LENGTH);
50+
});
51+
52+
it('Should fail to create profile if handle contains an invalid character before the suffix', async function () {
53+
await expect(
54+
profileCreationProxy.proxyCreateProfile({
55+
to: userAddress,
56+
handle: 'dots.are.invalid',
57+
imageURI: MOCK_PROFILE_URI,
58+
followModule: ZERO_ADDRESS,
59+
followModuleInitData: [],
60+
followNFTURI: MOCK_FOLLOW_NFT_URI,
61+
})
62+
).to.be.revertedWith(ERRORS.HANDLE_CONTAINS_INVALID_CHARACTERS);
63+
});
64+
65+
it('Should fail to create profile if handle starts with a dash, underscore or period', async function () {
66+
await expect(
67+
profileCreationProxy.proxyCreateProfile({
68+
to: userAddress,
69+
handle: '.abcdef',
70+
imageURI: MOCK_PROFILE_URI,
71+
followModule: ZERO_ADDRESS,
72+
followModuleInitData: [],
73+
followNFTURI: MOCK_FOLLOW_NFT_URI,
74+
})
75+
).to.be.revertedWith(ERRORS.HANDLE_FIRST_CHARACTER_INVALID);
76+
77+
await expect(
78+
profileCreationProxy.proxyCreateProfile({
79+
to: userAddress,
80+
handle: '-abcdef',
81+
imageURI: MOCK_PROFILE_URI,
82+
followModule: ZERO_ADDRESS,
83+
followModuleInitData: [],
84+
followNFTURI: MOCK_FOLLOW_NFT_URI,
85+
})
86+
).to.be.revertedWith(ERRORS.HANDLE_FIRST_CHARACTER_INVALID);
87+
88+
await expect(
89+
profileCreationProxy.proxyCreateProfile({
90+
to: userAddress,
91+
handle: '_abcdef',
92+
imageURI: MOCK_PROFILE_URI,
93+
followModule: ZERO_ADDRESS,
94+
followModuleInitData: [],
95+
followNFTURI: MOCK_FOLLOW_NFT_URI,
96+
})
97+
).to.be.revertedWith(ERRORS.HANDLE_FIRST_CHARACTER_INVALID);
98+
});
99+
});
100+
101+
context('Scenarios', function () {
102+
it('Should be able to create a profile using the whitelisted proxy, received NFT should be valid', async function () {
103+
let timestamp: any;
104+
let owner: string;
105+
let totalSupply: BigNumber;
106+
let profileId: BigNumber;
107+
let mintTimestamp: BigNumber;
108+
let tokenData: TokenDataStructOutput;
109+
const validHandleBeforeSuffix = 'v_al-id';
110+
const expectedHandle = 'v_al-id'.concat(REQUIRED_SUFFIX);
111+
112+
await expect(
113+
profileCreationProxy.proxyCreateProfile({
114+
to: userAddress,
115+
handle: validHandleBeforeSuffix,
116+
imageURI: MOCK_PROFILE_URI,
117+
followModule: ZERO_ADDRESS,
118+
followModuleInitData: [],
119+
followNFTURI: MOCK_FOLLOW_NFT_URI,
120+
})
121+
).to.not.be.reverted;
122+
123+
timestamp = await getTimestamp();
124+
owner = await lensHub.ownerOf(FIRST_PROFILE_ID);
125+
totalSupply = await lensHub.totalSupply();
126+
profileId = await lensHub.getProfileIdByHandle(expectedHandle);
127+
mintTimestamp = await lensHub.mintTimestampOf(FIRST_PROFILE_ID);
128+
tokenData = await lensHub.tokenDataOf(FIRST_PROFILE_ID);
129+
expect(owner).to.eq(userAddress);
130+
expect(totalSupply).to.eq(FIRST_PROFILE_ID);
131+
expect(profileId).to.eq(FIRST_PROFILE_ID);
132+
expect(mintTimestamp).to.eq(timestamp);
133+
expect(tokenData.owner).to.eq(userAddress);
134+
expect(tokenData.mintTimestamp).to.eq(timestamp);
135+
});
136+
});
137+
});

0 commit comments

Comments
 (0)