diff --git a/src/StationRegistry.sol b/src/StationRegistry.sol index 5aefdf7..c19b638 100644 --- a/src/StationRegistry.sol +++ b/src/StationRegistry.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.26; -import { BaseAccountFactory } from "@thirdweb/contracts/prebuilts/account/utils/BaseAccountFactory.sol"; import { IEntryPoint } from "@thirdweb/contracts/prebuilts/account/interface/IEntrypoint.sol"; import { PermissionsEnumerable } from "@thirdweb/contracts/extension/PermissionsEnumerable.sol"; import { EnumerableSet } from "@thirdweb/contracts/external-deps/openzeppelin/utils/structs/EnumerableSet.sol"; -import { IStationRegistry } from "./interfaces/IStationRegistry.sol"; import { Space } from "./Space.sol"; import { ModuleKeeper } from "./ModuleKeeper.sol"; import { Errors } from "./libraries/Errors.sol"; +import { IStationRegistry } from "./interfaces/IStationRegistry.sol"; +import { BaseAccountFactory } from "./utils/BaseAccountFactory.sol"; /// @title StationRegistry /// @notice See the documentation in {IStationRegistry} diff --git a/src/utils/BaseAccountFactory.sol b/src/utils/BaseAccountFactory.sol new file mode 100644 index 0000000..6892c94 --- /dev/null +++ b/src/utils/BaseAccountFactory.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +// Utils +import "@thirdweb-dev/contracts/extension/Multicall.sol"; +import "@thirdweb-dev/contracts/external-deps/openzeppelin/proxy/Clones.sol"; +import "@thirdweb-dev/contracts/external-deps/openzeppelin/utils/structs/EnumerableSet.sol"; +import "@thirdweb-dev/contracts/lib/BytesLib.sol"; + +// Interface +import "@thirdweb-dev/contracts/prebuilts/account/interface/IAccountFactory.sol"; + +// $$\ $$\ $$\ $$\ $$\ +// $$ | $$ | \__| $$ | $$ | +// $$$$$$\ $$$$$$$\ $$\ $$$$$$\ $$$$$$$ |$$\ $$\ $$\ $$$$$$\ $$$$$$$\ +// \_$$ _| $$ __$$\ $$ |$$ __$$\ $$ __$$ |$$ | $$ | $$ |$$ __$$\ $$ __$$\ +// $$ | $$ | $$ |$$ |$$ | \__|$$ / $$ |$$ | $$ | $$ |$$$$$$$$ |$$ | $$ | +// $$ |$$\ $$ | $$ |$$ |$$ | $$ | $$ |$$ | $$ | $$ |$$ ____|$$ | $$ | +// \$$$$ |$$ | $$ |$$ |$$ | \$$$$$$$ |\$$$$$\$$$$ |\$$$$$$$\ $$$$$$$ | +// \____/ \__| \__|\__|\__| \_______| \_____\____/ \_______|\_______/ + +/// Note: Fork of the thirdweb `BaseAccountFactory.sol` contract which allows the `createAccount` +/// method to be overriden in child contracts +abstract contract BaseAccountFactory is IAccountFactory, Multicall { + using EnumerableSet for EnumerableSet.AddressSet; + + /*/////////////////////////////////////////////////////////////// + State + //////////////////////////////////////////////////////////////*/ + + address public immutable accountImplementation; + address public immutable entrypoint; + + EnumerableSet.AddressSet private allAccounts; + mapping(address => EnumerableSet.AddressSet) internal accountsOfSigner; + + /*/////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////*/ + + constructor(address _accountImpl, address _entrypoint) { + accountImplementation = _accountImpl; + entrypoint = _entrypoint; + } + + /*/////////////////////////////////////////////////////////////// + Public functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Deploys a new Account for admin. + function createAccount(address _admin, bytes calldata _data) public virtual override returns (address) { + address impl = accountImplementation; + bytes32 salt = _generateSalt(_admin, _data); + address account = Clones.predictDeterministicAddress(impl, salt); + + if (account.code.length > 0) { + return account; + } + + account = Clones.cloneDeterministic(impl, salt); + + if (msg.sender != entrypoint) { + require(allAccounts.add(account), "AccountFactory: account already registered"); + } + + _initializeAccount(account, _admin, _data); + + emit AccountCreated(account, _admin); + + return account; + } + + /*/////////////////////////////////////////////////////////////// + External functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Callback function for an Account to register itself on the factory. + function onRegister( + bytes32 _salt + ) external { + address account = msg.sender; + require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account."); + + require(allAccounts.add(account), "AccountFactory: account already registered"); + } + + function onSignerAdded(address _signer, bytes32 _salt) external { + address account = msg.sender; + require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account."); + + bool isNewSigner = accountsOfSigner[_signer].add(account); + + if (isNewSigner) { + emit SignerAdded(account, _signer); + } + } + + /// @notice Callback function for an Account to un-register its signers. + function onSignerRemoved(address _signer, bytes32 _salt) external { + address account = msg.sender; + require(_isAccountOfFactory(account, _salt), "AccountFactory: not an account."); + + bool isAccount = accountsOfSigner[_signer].remove(account); + + if (isAccount) { + emit SignerRemoved(account, _signer); + } + } + + /*/////////////////////////////////////////////////////////////// + View functions + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns whether an account is registered on this factory. + function isRegistered( + address _account + ) external view returns (bool) { + return allAccounts.contains(_account); + } + + /// @notice Returns the total number of accounts. + function totalAccounts() external view returns (uint256) { + return allAccounts.length(); + } + + /// @notice Returns all accounts between the given indices. + function getAccounts(uint256 _start, uint256 _end) external view returns (address[] memory accounts) { + require(_start < _end && _end <= allAccounts.length(), "BaseAccountFactory: invalid indices"); + + uint256 len = _end - _start; + accounts = new address[](_end - _start); + + for (uint256 i = 0; i < len; i += 1) { + accounts[i] = allAccounts.at(i + _start); + } + } + + /// @notice Returns all accounts created on the factory. + function getAllAccounts() external view returns (address[] memory) { + return allAccounts.values(); + } + + /// @notice Returns the address of an Account that would be deployed with the given admin signer. + function getAddress(address _adminSigner, bytes calldata _data) public view returns (address) { + bytes32 salt = _generateSalt(_adminSigner, _data); + return Clones.predictDeterministicAddress(accountImplementation, salt); + } + + /// @notice Returns all accounts that the given address is a signer of. + function getAccountsOfSigner( + address signer + ) external view returns (address[] memory accounts) { + return accountsOfSigner[signer].values(); + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns whether the caller is an account deployed by this factory. + function _isAccountOfFactory(address _account, bytes32 _salt) internal view virtual returns (bool) { + address predicted = Clones.predictDeterministicAddress(accountImplementation, _salt); + return _account == predicted; + } + + function _getImplementation( + address cloneAddress + ) internal view returns (address) { + bytes memory code = cloneAddress.code; + return BytesLib.toAddress(code, 10); + } + + /// @dev Returns the salt used when deploying an Account. + function _generateSalt(address _admin, bytes memory _data) internal view virtual returns (bytes32) { + return keccak256(abi.encode(_admin, _data)); + } + + /// @dev Called in `createAccount`. Initializes the account contract created in `createAccount`. + function _initializeAccount(address _account, address _admin, bytes calldata _data) internal virtual; +}