Skip to content

Commit c778b3c

Browse files
authored
Proxy pattern smart wallet factory contracts (#562)
* Enable multiple accounts per admin by not hardcoding _data * Update sender address in tests * initial unit test * Update unit test: create multiple accounts with same admin * Add test for Dynamic and Managed smart wallets * Use abi.encode instead of encodePacked * Move deposit fns to AccountExtension * Create BaseAccountFactoryStorage * Make account factory contracts initializable * Fix build errors in tests * Store factory in initialize fn * Update tests
1 parent 80fa323 commit c778b3c

17 files changed

+236
-74
lines changed

contracts/prebuilts/account/dynamic/DynamicAccount.sol

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,23 @@ contract DynamicAccount is AccountCore, BaseRouter {
2424
//////////////////////////////////////////////////////////////*/
2525

2626
constructor(IEntryPoint _entrypoint, Extension[] memory _defaultExtensions)
27-
AccountCore(_entrypoint, msg.sender)
27+
AccountCore(_entrypoint)
2828
BaseRouter(_defaultExtensions)
2929
{
3030
_disableInitializers();
3131
}
3232

3333
/// @notice Initializes the smart contract wallet.
34-
function initialize(address _defaultAdmin, bytes calldata _data) public override initializer {
34+
function initialize(
35+
address _defaultAdmin,
36+
address _factory,
37+
bytes calldata _data
38+
) public override initializer {
3539
__BaseRouter_init();
40+
41+
// This is passed as data in the `_registerOnFactory()` call in `AccountExtension` / `Account`.
3642
AccountCoreStorage.data().creationSalt = _generateSalt(_defaultAdmin, _data);
43+
AccountCoreStorage.data().factory = _factory;
3744
_setAdmin(_defaultAdmin, true);
3845
}
3946

contracts/prebuilts/account/dynamic/DynamicAccountFactory.sol

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ pragma solidity ^0.8.12;
44
// Utils
55
import "../utils/BaseAccountFactory.sol";
66
import "@thirdweb-dev/dynamic-contracts/src/interface/IExtension.sol";
7+
import "../../../extension/upgradeable/Initializable.sol";
78

89
// Extensions
9-
import "../../../extension/upgradeable//PermissionsEnumerable.sol";
10-
import "../../../extension/upgradeable//ContractMetadata.sol";
10+
import "../../../extension/upgradeable/PermissionsEnumerable.sol";
11+
import "../../../extension/upgradeable/ContractMetadata.sol";
1112

1213
// Smart wallet implementation
1314
import { DynamicAccount, IEntryPoint } from "./DynamicAccount.sol";
@@ -21,20 +22,24 @@ import { DynamicAccount, IEntryPoint } from "./DynamicAccount.sol";
2122
// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ |
2223
// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/
2324

24-
contract DynamicAccountFactory is BaseAccountFactory, ContractMetadata, PermissionsEnumerable {
25+
contract DynamicAccountFactory is Initializable, BaseAccountFactory, ContractMetadata, PermissionsEnumerable {
2526
address public constant ENTRYPOINT_ADDRESS = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789;
2627

2728
/*///////////////////////////////////////////////////////////////
2829
Constructor
2930
//////////////////////////////////////////////////////////////*/
3031

31-
constructor(address _defaultAdmin, IExtension.Extension[] memory _defaultExtensions)
32+
constructor(IExtension.Extension[] memory _defaultExtensions)
3233
BaseAccountFactory(
33-
payable(address(new DynamicAccount(IEntryPoint(ENTRYPOINT_ADDRESS), _defaultExtensions))),
34+
address(new DynamicAccount(IEntryPoint(ENTRYPOINT_ADDRESS), _defaultExtensions)),
3435
ENTRYPOINT_ADDRESS
3536
)
36-
{
37+
{}
38+
39+
/// @notice Initializes the factory contract.
40+
function initialize(address _defaultAdmin, string memory _contractURI) external initializer {
3741
_setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
42+
_setupContractURI(_contractURI);
3843
}
3944

4045
/*///////////////////////////////////////////////////////////////
@@ -47,7 +52,7 @@ contract DynamicAccountFactory is BaseAccountFactory, ContractMetadata, Permissi
4752
address _admin,
4853
bytes calldata _data
4954
) internal override {
50-
DynamicAccount(payable(_account)).initialize(_admin, _data);
55+
DynamicAccount(payable(_account)).initialize(_admin, address(this), _data);
5156
}
5257

5358
/// @dev Returns whether contract metadata can be set in the given execution context.

contracts/prebuilts/account/managed/ManagedAccount.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ import "@thirdweb-dev/dynamic-contracts/src/core/Router.sol";
1919
import "@thirdweb-dev/dynamic-contracts/src/interface/IRouterState.sol";
2020

2121
contract ManagedAccount is AccountCore, Router, IRouterState {
22-
constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) {}
22+
constructor(IEntryPoint _entrypoint) AccountCore(_entrypoint) {}
2323

2424
/// @notice Returns the implementation contract address for a given function signature.
2525
function getImplementationForFunction(bytes4 _functionSelector) public view virtual override returns (address) {
26-
return Router(payable(factory)).getImplementationForFunction(_functionSelector);
26+
return Router(payable(AccountCoreStorage.data().factory)).getImplementationForFunction(_functionSelector);
2727
}
2828

2929
/// @notice Returns all extensions of the Router.
3030
function getAllExtensions() external view returns (Extension[] memory) {
31-
return IRouterState(payable(factory)).getAllExtensions();
31+
return IRouterState(payable(AccountCoreStorage.data().factory)).getAllExtensions();
3232
}
3333
}

contracts/prebuilts/account/managed/ManagedAccountFactory.sol

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity ^0.8.12;
44
// Utils
55
import "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol";
66
import "../utils/BaseAccountFactory.sol";
7+
import "../../../extension/upgradeable/Initializable.sol";
78

89
// Extensions
910
import "../../../extension/upgradeable//PermissionsEnumerable.sol";
@@ -21,25 +22,33 @@ import { ManagedAccount, IEntryPoint } from "./ManagedAccount.sol";
2122
// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ |
2223
// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/
2324

24-
contract ManagedAccountFactory is BaseAccountFactory, ContractMetadata, PermissionsEnumerable, BaseRouter {
25+
contract ManagedAccountFactory is
26+
Initializable,
27+
BaseAccountFactory,
28+
ContractMetadata,
29+
PermissionsEnumerable,
30+
BaseRouter
31+
{
2532
/*///////////////////////////////////////////////////////////////
2633
Constructor
2734
//////////////////////////////////////////////////////////////*/
2835

29-
constructor(
30-
address _defaultAdmin,
31-
IEntryPoint _entrypoint,
32-
Extension[] memory _defaultExtensions
33-
)
36+
constructor(IEntryPoint _entrypoint, Extension[] memory _defaultExtensions)
3437
BaseRouter(_defaultExtensions)
35-
BaseAccountFactory(payable(address(new ManagedAccount(_entrypoint, address(this)))), address(_entrypoint))
36-
{
38+
BaseAccountFactory(address(new ManagedAccount(_entrypoint)), address(_entrypoint))
39+
{}
40+
41+
/// @notice Initializes the factory contract.
42+
function initialize(address _defaultAdmin, string memory _contractURI) external initializer {
3743
__BaseRouter_init();
44+
3845
_setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
3946

4047
bytes32 _extensionRole = keccak256("EXTENSION_ROLE");
4148
_setupRole(_extensionRole, _defaultAdmin);
4249
_setRoleAdmin(_extensionRole, _extensionRole);
50+
51+
_setupContractURI(_contractURI);
4352
}
4453

4554
/*///////////////////////////////////////////////////////////////
@@ -52,7 +61,7 @@ contract ManagedAccountFactory is BaseAccountFactory, ContractMetadata, Permissi
5261
address _admin,
5362
bytes calldata _data
5463
) internal override {
55-
ManagedAccount(payable(_account)).initialize(_admin, _data);
64+
ManagedAccount(payable(_account)).initialize(_admin, address(this), _data);
5665
}
5766

5867
/// @dev Returns whether all relevant permission and other checks are met before any upgrade.

contracts/prebuilts/account/non-upgradeable/Account.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ contract Account is AccountCore, ContractMetadata, ERC1271, ERC721Holder, ERC115
3737
Constructor, Initializer, Modifiers
3838
//////////////////////////////////////////////////////////////*/
3939

40-
constructor(IEntryPoint _entrypoint, address _factory) AccountCore(_entrypoint, _factory) {}
40+
constructor(IEntryPoint _entrypoint) AccountCore(_entrypoint) {}
4141

4242
/// @notice Checks whether the caller is the EntryPoint contract or the admin.
4343
modifier onlyAdminOrEntrypoint() virtual {
@@ -132,7 +132,7 @@ contract Account is AccountCore, ContractMetadata, ERC1271, ERC721Holder, ERC115
132132

133133
/// @dev Registers the account on the factory if it hasn't been registered yet.
134134
function _registerOnFactory() internal virtual {
135-
BaseAccountFactory factoryContract = BaseAccountFactory(factory);
135+
BaseAccountFactory factoryContract = BaseAccountFactory(AccountCoreStorage.data().factory);
136136
if (!factoryContract.isRegistered(address(this))) {
137137
factoryContract.onRegister(AccountCoreStorage.data().creationSalt);
138138
}

contracts/prebuilts/account/non-upgradeable/AccountFactory.sol

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pragma solidity ^0.8.12;
55
import "../utils/BaseAccountFactory.sol";
66
import "../utils/BaseAccount.sol";
77
import "../../../external-deps/openzeppelin/proxy/Clones.sol";
8+
import "../../../extension/upgradeable/Initializable.sol";
89

910
// Extensions
1011
import "../../../extension/upgradeable//PermissionsEnumerable.sol";
@@ -25,15 +26,17 @@ import { Account } from "./Account.sol";
2526
// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ |
2627
// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/
2728

28-
contract AccountFactory is BaseAccountFactory, ContractMetadata, PermissionsEnumerable {
29+
contract AccountFactory is Initializable, BaseAccountFactory, ContractMetadata, PermissionsEnumerable {
2930
/*///////////////////////////////////////////////////////////////
3031
Constructor
3132
//////////////////////////////////////////////////////////////*/
3233

33-
constructor(address _defaultAdmin, IEntryPoint _entrypoint)
34-
BaseAccountFactory(address(new Account(_entrypoint, address(this))), address(_entrypoint))
35-
{
34+
constructor(IEntryPoint _entrypoint) BaseAccountFactory(address(new Account(_entrypoint)), address(_entrypoint)) {}
35+
36+
/// @notice Initializes the factory contract.
37+
function initialize(address _defaultAdmin, string memory _contractURI) external initializer {
3638
_setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
39+
_setupContractURI(_contractURI);
3740
}
3841

3942
/*///////////////////////////////////////////////////////////////
@@ -46,7 +49,7 @@ contract AccountFactory is BaseAccountFactory, ContractMetadata, PermissionsEnum
4649
address _admin,
4750
bytes calldata _data
4851
) internal override {
49-
Account(payable(_account)).initialize(_admin, _data);
52+
Account(payable(_account)).initialize(_admin, address(this), _data);
5053
}
5154

5255
/// @dev Returns whether contract metadata can be set in the given execution context.

contracts/prebuilts/account/utils/AccountCore.sol

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,33 +39,39 @@ contract AccountCore is IAccountCore, Initializable, Multicall, BaseAccount, Acc
3939
State
4040
//////////////////////////////////////////////////////////////*/
4141

42-
/// @notice EIP 4337 factory for this contract.
43-
address public immutable factory;
44-
4542
/// @notice EIP 4337 Entrypoint contract.
4643
IEntryPoint private immutable entrypointContract;
4744

4845
/*///////////////////////////////////////////////////////////////
4946
Constructor, Initializer, Modifiers
5047
//////////////////////////////////////////////////////////////*/
5148

52-
constructor(IEntryPoint _entrypoint, address _factory) EIP712("Account", "1") {
49+
constructor(IEntryPoint _entrypoint) EIP712("Account", "1") {
5350
_disableInitializers();
54-
factory = _factory;
5551
entrypointContract = _entrypoint;
5652
}
5753

5854
/// @notice Initializes the smart contract wallet.
59-
function initialize(address _defaultAdmin, bytes calldata _data) public virtual initializer {
55+
function initialize(
56+
address _defaultAdmin,
57+
address _factory,
58+
bytes calldata _data
59+
) public virtual initializer {
6060
// This is passed as data in the `_registerOnFactory()` call in `AccountExtension` / `Account`.
6161
AccountCoreStorage.data().creationSalt = _generateSalt(_defaultAdmin, _data);
62+
AccountCoreStorage.data().factory = _factory;
6263
_setAdmin(_defaultAdmin, true);
6364
}
6465

6566
/*///////////////////////////////////////////////////////////////
6667
View functions
6768
//////////////////////////////////////////////////////////////*/
6869

70+
/// @notice Returns the address of the account factory.
71+
function factory() public view virtual override returns (address) {
72+
return AccountCoreStorage.data().factory;
73+
}
74+
6975
/// @notice Returns the EIP 4337 entrypoint contract.
7076
function entryPoint() public view virtual override returns (IEntryPoint) {
7177
address entrypointOverride = AccountCoreStorage.data().entrypointOverride;
@@ -235,19 +241,22 @@ contract AccountCore is IAccountCore, Initializable, Multicall, BaseAccount, Acc
235241
/// @notice Makes the given account an admin.
236242
function _setAdmin(address _account, bool _isAdmin) internal virtual override {
237243
super._setAdmin(_account, _isAdmin);
238-
if (factory.code.length > 0) {
244+
245+
address factoryAddr = factory();
246+
if (factoryAddr.code.length > 0) {
239247
if (_isAdmin) {
240-
BaseAccountFactory(factory).onSignerAdded(_account, AccountCoreStorage.data().creationSalt);
248+
BaseAccountFactory(factoryAddr).onSignerAdded(_account, AccountCoreStorage.data().creationSalt);
241249
} else {
242-
BaseAccountFactory(factory).onSignerRemoved(_account, AccountCoreStorage.data().creationSalt);
250+
BaseAccountFactory(factoryAddr).onSignerRemoved(_account, AccountCoreStorage.data().creationSalt);
243251
}
244252
}
245253
}
246254

247255
/// @notice Runs after every `changeRole` run.
248256
function _afterSignerPermissionsUpdate(SignerPermissionRequest calldata _req) internal virtual override {
249-
if (factory.code.length > 0) {
250-
BaseAccountFactory(factory).onSignerAdded(_req.signer, AccountCoreStorage.data().creationSalt);
257+
address factoryAddr = factory();
258+
if (factoryAddr.code.length > 0) {
259+
BaseAccountFactory(factoryAddr).onSignerAdded(_req.signer, AccountCoreStorage.data().creationSalt);
251260
}
252261
}
253262
}

contracts/prebuilts/account/utils/AccountCoreStorage.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ library AccountCoreStorage {
99

1010
struct Data {
1111
address entrypointOverride;
12+
address factory;
1213
bytes32 creationSalt;
1314
}
1415

contracts/prebuilts/account/utils/BaseAccountFactory.sol

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.12;
33

44
// Utils
5+
import "./BaseAccountFactoryStorage.sol";
56
import "../../../extension/Multicall.sol";
67
import "../../../external-deps/openzeppelin/proxy/Clones.sol";
78
import "../../../external-deps/openzeppelin/utils/structs/EnumerableSet.sol";
@@ -32,9 +33,6 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall {
3233
address public immutable accountImplementation;
3334
address public immutable entrypoint;
3435

35-
EnumerableSet.AddressSet private allAccounts;
36-
mapping(address => EnumerableSet.AddressSet) internal accountsOfSigner;
37-
3836
/*///////////////////////////////////////////////////////////////
3937
Constructor
4038
//////////////////////////////////////////////////////////////*/
@@ -61,7 +59,10 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall {
6159
account = Clones.cloneDeterministic(impl, salt);
6260

6361
if (msg.sender != entrypoint) {
64-
require(allAccounts.add(account), "AccountFactory: account already registered");
62+
require(
63+
_baseAccountFactoryStorage().allAccounts.add(account),
64+
"AccountFactory: account already registered"
65+
);
6566
}
6667

6768
_initializeAccount(account, _admin, _data);
@@ -76,14 +77,14 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall {
7677
address account = msg.sender;
7778
require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account.");
7879

79-
require(allAccounts.add(account), "AccountFactory: account already registered");
80+
require(_baseAccountFactoryStorage().allAccounts.add(account), "AccountFactory: account already registered");
8081
}
8182

8283
function onSignerAdded(address _signer, bytes32 _salt) external {
8384
address account = msg.sender;
8485
require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account.");
8586

86-
bool isNewSigner = accountsOfSigner[_signer].add(account);
87+
bool isNewSigner = _baseAccountFactoryStorage().accountsOfSigner[_signer].add(account);
8788

8889
if (isNewSigner) {
8990
emit SignerAdded(account, _signer);
@@ -95,7 +96,7 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall {
9596
address account = msg.sender;
9697
require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account.");
9798

98-
bool isAccount = accountsOfSigner[_signer].remove(account);
99+
bool isAccount = _baseAccountFactoryStorage().accountsOfSigner[_signer].remove(account);
99100

100101
if (isAccount) {
101102
emit SignerRemoved(account, _signer);
@@ -108,12 +109,12 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall {
108109

109110
/// @notice Returns whether an account is registered on this factory.
110111
function isRegistered(address _account) external view returns (bool) {
111-
return allAccounts.contains(_account);
112+
return _baseAccountFactoryStorage().allAccounts.contains(_account);
112113
}
113114

114115
/// @notice Returns all accounts created on the factory.
115116
function getAllAccounts() external view returns (address[] memory) {
116-
return allAccounts.values();
117+
return _baseAccountFactoryStorage().allAccounts.values();
117118
}
118119

119120
/// @notice Returns the address of an Account that would be deployed with the given admin signer.
@@ -124,7 +125,7 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall {
124125

125126
/// @notice Returns all accounts that the given address is a signer of.
126127
function getAccountsOfSigner(address signer) external view returns (address[] memory accounts) {
127-
return accountsOfSigner[signer].values();
128+
return _baseAccountFactoryStorage().accountsOfSigner[signer].values();
128129
}
129130

130131
/*///////////////////////////////////////////////////////////////
@@ -147,6 +148,11 @@ abstract contract BaseAccountFactory is IAccountFactory, Multicall {
147148
return keccak256(abi.encode(_admin, _data));
148149
}
149150

151+
/// @dev Returns the BaseAccountFactory contract's storage.
152+
function _baseAccountFactoryStorage() internal pure returns (BaseAccountFactoryStorage.Data storage) {
153+
return BaseAccountFactoryStorage.data();
154+
}
155+
150156
/// @dev Called in `createAccount`. Initializes the account contract created in `createAccount`.
151157
function _initializeAccount(
152158
address _account,

0 commit comments

Comments
 (0)