Skip to content

Instantly share code, notes, and snippets.

@MatiFalcone
Created December 29, 2022 07:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MatiFalcone/c0f6c5c82a59d8628160a5a9e372ccc4 to your computer and use it in GitHub Desktop.
Save MatiFalcone/c0f6c5c82a59d8628160a5a9e372ccc4 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.11;
// ========== External imports ==========
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol";
// ========== Internal imports ==========
import "../interfaces/ISmartWallet.sol";
import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol";
// ========== Features ==========
import "../extension/ContractMetadata.sol";
import "../extension/Royalty.sol";
import "../extension/ThirdOwnable.sol";
import "../extension/PermissionsEnumerable.sol";
import { TokenStore, ERC1155ReceiverUpgradeable, IERC1155ReceiverUpgradeable } from "../extension/TokenStore.sol";
// ========== Proxy ==========
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract SmartWallet is
Initializable,
ContractMetadata,
Royalty,
ThirdOwnable,
PermissionsEnumerable,
TokenStore,
ReentrancyGuardUpgradeable,
ERC2771ContextUpgradeable,
ERC721EnumerableUpgradeable,
ISmartWallet,
UUPSUpgradeable
{
/*///////////////////////////////////////////////////////////////
State variables
//////////////////////////////////////////////////////////////*/
bytes32 private constant MODULE_TYPE = bytes32("Multiwrap");
uint256 private constant VERSION = 1;
/// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted.
bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE");
/// @dev Only MINTER_ROLE holders can wrap tokens, when wrapping is restricted.
bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE");
/// @dev Only UNWRAP_ROLE holders can unwrap tokens, when unwrapping is restricted.
bytes32 private constant UNWRAP_ROLE = keccak256("UNWRAP_ROLE");
/// @dev Only assets with ASSET_ROLE can be wrapped, when wrapping is restricted to particular assets.
bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE");
/// @dev The next token ID of the NFT to mint.
uint256 public nextTokenIdToMint;
/*///////////////////////////////////////////////////////////////
Constructor + initializer logic
//////////////////////////////////////////////////////////////*/
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) initializer {} // 0x9c3C9283D3e44854697Cd22D3Faa240Cfb032889
/// @dev Initiliazes the contract, like a constructor.
function initialize(
address _defaultAdmin,
string memory _name,
string memory _symbol,
string memory _contractURI,
address[] memory _trustedForwarders,
address _royaltyRecipient,
uint256 _royaltyBps
) external initializer {
// Initialize inherited contracts, most base-like -> most derived.
__ReentrancyGuard_init();
__ERC2771Context_init(_trustedForwarders);
__ERC721_init(_name, _symbol);
// Initialize this contract's state.
_setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps);
_setupOwner(_defaultAdmin);
_setupContractURI(_contractURI);
_setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin);
_setupRole(MINTER_ROLE, _defaultAdmin);
_setupRole(TRANSFER_ROLE, _defaultAdmin);
// note: see `_beforeTokenTransfer` for TRANSFER_ROLE behaviour.
_setupRole(TRANSFER_ROLE, address(0));
// note: see `onlyRoleWithSwitch` for UNWRAP_ROLE behaviour.
_setupRole(UNWRAP_ROLE, address(0));
// note: see `onlyRoleWithSwitch` for UNWRAP_ROLE behaviour.
_setupRole(ASSET_ROLE, address(0));
// note: mints empty Smart NFT
mintSmartNFT(_defaultAdmin);
}
///@dev required by the OZ UUPS module
function _authorizeUpgrade(address) internal override onlyOwner {}
/*///////////////////////////////////////////////////////////////
Modifiers
//////////////////////////////////////////////////////////////*/
modifier onlyRoleWithSwitch(bytes32 role) {
_checkRoleWithSwitch(role, _msgSender());
_;
}
/*///////////////////////////////////////////////////////////////
Generic contract logic
//////////////////////////////////////////////////////////////*/
/// @dev Returns the type of the contract.
function contractType() external pure returns (bytes32) {
return MODULE_TYPE;
}
/// @dev Returns the version of the contract.
function contractVersion() external pure returns (uint8) {
return uint8(VERSION);
}
/// @dev Lets the contract receive ether to unwrap native tokens.
receive() external payable {
require(msg.sender == nativeTokenWrapper, "caller not native token wrapper.");
}
/*///////////////////////////////////////////////////////////////
ERC 165 / 721 / 2981 logic
//////////////////////////////////////////////////////////////*/
/// @dev Returns the URI for a given tokenId.
function tokenURI(uint256 _tokenId) public view override returns (string memory) {
return getUriOfBundle(_tokenId);
}
/// @dev See ERC 165
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC1155ReceiverUpgradeable, ERC721EnumerableUpgradeable, IERC165Upgradeable)
returns (bool)
{
return
super.supportsInterface(interfaceId) ||
interfaceId == type(IERC721Upgradeable).interfaceId ||
interfaceId == type(IERC1155ReceiverUpgradeable).interfaceId ||
interfaceId == type(IERC2981Upgradeable).interfaceId;
}
/*///////////////////////////////////////////////////////////////
Minting logic
//////////////////////////////////////////////////////////////*/
/// @dev Mint an empty ERC721 NFT that can later wrap multiple ERC1155, ERC721, ERC20 tokens.
function mintSmartNFT(
address _recipient
) public nonReentrant onlyRoleWithSwitch(MINTER_ROLE) returns (uint256 tokenId) {
Token[] memory noTokens;
tokenId = nextTokenIdToMint;
nextTokenIdToMint += 1;
_safeMint(_recipient, tokenId);
emit SmartWalletCreated(_msgSender(), _recipient, tokenId, noTokens);
}
/*///////////////////////////////////////////////////////////////
Loading / Unloading logic
//////////////////////////////////////////////////////////////*/
/// @dev Load multiple ERC1155, ERC721, ERC20 tokens into empty Smart NFT
function initialLoad(
Token[] calldata _tokensToLoad,
uint256 _smartNFTId,
string calldata _uriForLoadedToken
) external payable nonReentrant onlyRoleWithSwitch(MINTER_ROLE) {
require(getTokenCountOfBundle(_smartNFTId) == 0, "Smart NFT not empty. Call loadTokens instead.");
if (!hasRole(ASSET_ROLE, address(0))) {
for (uint256 i = 0; i < _tokensToLoad.length; i += 1) {
_checkRole(ASSET_ROLE, _tokensToLoad[i].assetContract);
}
}
// check that Smart NFT exists
require(_smartNFTId < nextTokenIdToMint, "Smart NFT DNE.");
_storeTokens(_msgSender(), _tokensToLoad, _uriForLoadedToken, _smartNFTId);
emit TokensLoaded(_msgSender(), _smartNFTId, _tokensToLoad);
}
/// @dev Load multiple ERC1155, ERC721, ERC20 tokens into a non-empty Smart NFT (bundle already created).
function loadTokens(
Token[] calldata _tokensToLoad,
uint256 _smartNFTId
) external payable nonReentrant onlyRoleWithSwitch(MINTER_ROLE) {
require(getTokenCountOfBundle(_smartNFTId) > 0, "Smart NFT is empty. Call initialLoad instead.");
require(_smartNFTId < nextTokenIdToMint, "Smart NFT DNE.");
require(_isApprovedOrOwner(_msgSender(), _smartNFTId), "caller not approved for loading this Smart NFT.");
if (!hasRole(ASSET_ROLE, address(0))) {
for (uint256 i = 0; i < _tokensToLoad.length; i += 1) {
_checkRole(ASSET_ROLE, _tokensToLoad[i].assetContract);
}
}
for (uint256 i = 0; i < _tokensToLoad.length; i += 1) {
_addTokenInBundle(_tokensToLoad[i], _smartNFTId);
_transferToken(_msgSender(), address(this), _tokensToLoad[i]);
}
emit TokensLoaded(_msgSender(), _smartNFTId, _tokensToLoad);
}
/// @dev Unload from Smart Wallet to retrieve one underlying ERC1155, ERC721, ERC20 token.
function unloadToken(uint256 _smartNFTId, Token calldata _tokenToRetrieve, address _recipient) external nonReentrant onlyRoleWithSwitch(UNWRAP_ROLE) {
require(_smartNFTId < nextTokenIdToMint, "Smart NFT DNE.");
require(_isApprovedOrOwner(_msgSender(), _smartNFTId), "caller not approved for unloading.");
_releaseToken(_recipient, _smartNFTId, _tokenToRetrieve);
emit TokenUnloaded(_msgSender(), _recipient, _smartNFTId, _tokenToRetrieve);
}
/// @dev Unload from Smart Wallet to retrieve all underlying ERC1155, ERC721, ERC20 tokens.
function unloadAll(uint256 _smartNFTId, address _recipient) external nonReentrant onlyRoleWithSwitch(UNWRAP_ROLE) {
require(_smartNFTId < nextTokenIdToMint, "Smart NFT DNE.");
require(_isApprovedOrOwner(_msgSender(), _smartNFTId), "caller not approved for unloading.");
_releaseTokens(_recipient, _smartNFTId);
emit AllTokensUnloaded(_msgSender(), _recipient, _smartNFTId);
}
/*///////////////////////////////////////////////////////////////
Getter functions
//////////////////////////////////////////////////////////////*/
/// @dev Returns the underlying contents of a wrapped NFT.
function getWrappedContents(uint256 _tokenId) external view returns (Token[] memory contents) {
uint256 total = getTokenCountOfBundle(_tokenId);
contents = new Token[](total);
for (uint256 i = 0; i < total; i += 1) {
contents[i] = getTokenOfBundle(_tokenId, i);
}
}
/*///////////////////////////////////////////////////////////////
Internal functions
//////////////////////////////////////////////////////////////*/
/// @dev Returns whether owner can be set in the given execution context.
function _canSetOwner() internal view override returns (bool) {
return hasRole(DEFAULT_ADMIN_ROLE, _msgSender());
}
/// @dev Returns whether royalty info can be set in the given execution context.
function _canSetRoyaltyInfo() internal view override returns (bool) {
return hasRole(DEFAULT_ADMIN_ROLE, _msgSender());
}
/// @dev Returns whether contract metadata can be set in the given execution context.
function _canSetContractURI() internal view override returns (bool) {
return hasRole(DEFAULT_ADMIN_ROLE, _msgSender());
}
/*///////////////////////////////////////////////////////////////
Miscellaneous
//////////////////////////////////////////////////////////////*/
/**
* @dev See {ERC721-_beforeTokenTransfer}.
*/
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual {
super._beforeTokenTransfer(from, to, tokenId, 1);
// if transfer is restricted on the contract, we still want to allow burning and minting
if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) {
require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "!TRANSFER_ROLE");
}
}
function _msgSender()
internal
view
virtual
override(ContextUpgradeable, ERC2771ContextUpgradeable)
returns (address sender)
{
return ERC2771ContextUpgradeable._msgSender();
}
function _msgData()
internal
view
virtual
override(ContextUpgradeable, ERC2771ContextUpgradeable)
returns (bytes calldata)
{
return ERC2771ContextUpgradeable._msgData();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
import "../proxy/SmartWalletProxy.sol";
import "../multiwrap/SmartWallet.sol";
contract SmartWalletFactory is Initializable, OwnableUpgradeable {
using CountersUpgradeable for CountersUpgradeable.Counter;
mapping(uint256 => address) private smartWallets;
CountersUpgradeable.Counter private counter;
string private version;
address implementation;
function initialize(address smartWallet, string memory _version) public initializer {
//_transferOwnership(tx.origin);
version = _version;
implementation = smartWallet;
}
function versionContract() public view returns (string memory) {
return version;
}
function createSmartWallet(
string memory _name,
string memory _symbol,
string memory _contractURI,
address[] memory _trustedForwarders,
address _royaltyRecipient,
uint256 _royaltyBps
) public {
SmartWalletProxy newSmartWallet = new SmartWalletProxy(address(implementation), abi.encodeWithSelector(SmartWallet(payable(address(0))).initialize.selector, msg.sender, _name, _symbol, _contractURI, _trustedForwarders, _royaltyRecipient, _royaltyBps));
uint256 currentCounter = counter.current();
smartWallets[currentCounter] = address(newSmartWallet);
counter.increment();
}
function getSmartWalletAddress(uint256 id) public view returns(address) {
return smartWallets[id];
}
function getCurrentCounter() public view returns(uint256) {
return counter.current();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/proxy/Proxy.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
// Kept for backwards compatibility with older versions of Hardhat and Truffle plugins.
contract SmartWalletProxy is ERC1967Proxy, Ownable {
constructor(address _logic, bytes memory _data) ERC1967Proxy(_logic, _data) {
transferOwnership(tx.origin);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment