Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

limited supply extension factory #77

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/extensions/base/LimitedSupply.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import "../../interfaces/IERC721Community.sol";

abstract contract LimitedSupply is INFTExtension {

uint256 private totalMinted;
uint256 public totalMinted;
uint256 public immutable extensionSupply;

constructor(uint256 _extensionSupply) {
Expand Down
74 changes: 74 additions & 0 deletions contracts/extensions/factories/LimitedSupplyExtension.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

import "./base/NFTExtensionUpgradeable.sol";
import "./base/SaleControlUpgradeable.sol";
import "./base/LimitedSupplyUpgradeable.sol";

interface NFT is IERC721Community {
function maxSupply() external view returns (uint256);

function totalSupply() external view returns (uint256);
}

contract LimitedSupplyExtension is
NFTExtensionUpgradeable,
OwnableUpgradeable,
SaleControlUpgradeable,
LimitedSupplyUpgradeable
{
uint256 public price;
uint256 public maxPerMint;
uint256 public maxPerWallet;

string public title;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}

function initialize(
string memory _title,
address _nft,
uint256 _price,
uint256 _maxPerMint,
uint256 _maxPerWallet,
uint256 _extensionSupply
) public initializer {
NFTExtensionUpgradeable.initialize(_nft);
SaleControlUpgradeable.initialize();
LimitedSupplyUpgradeable.initialize(_extensionSupply);

title = _title;
price = _price;
maxPerMint = _maxPerMint;
maxPerWallet = _maxPerWallet;
}

function mint(uint256 amount)
external
payable
whenSaleStarted
whenLimitedSupplyNotReached(amount)
{
require(
IERC721(address(nft)).balanceOf(msg.sender) + amount <=
maxPerWallet,
"LimitedSupplyMintingExtension: max per wallet reached"
);

require(amount <= maxPerMint, "Too many tokens to mint");
require(msg.value >= amount * price, "Not enough ETH to mint");

nft.mintExternal{value: msg.value}(amount, msg.sender, bytes32(0x0));
}

function maxSupply() public view returns (uint256) {
return NFT(address(nft)).maxSupply();
}

function totalSupply() public view returns (uint256) {
return NFT(address(nft)).totalSupply();
}
}
126 changes: 126 additions & 0 deletions contracts/extensions/factories/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
## NFT Extension Factories

update this README according to this block:
- factories deploy minting extensions
- `base` folder has extensions building blocks, you inherit them
- tell about Allowlist and LimitedSupplyExtension
- how to check extensions match their non-upgradeable versions using `colordiff`

## How it works

Factory is a contract that deploys extensions. It has two functions:

- `createExtension` deploys an extension and returns its address
- `createExtensionAndCall` deploys an extension and calls a function on it

Both functions have the same signature:

```solidity
function createExtensionAndCall(
address _implementation,
bytes memory _data
) public returns (address extension);
```

`_implementation` is the address of the extension implementation. `_data` is the data that is passed to the extension's constructor. It contains the parameters of the extension.

## Extensions

### AllowlistExtension

This extension allows to set an allowlist of addresses that can mint NFTs. It also allows to set a maximum number of NFTs that can be minted.

### LimitedSupplyExtension

This extension allows to set a maximum number of NFTs that can be minted.

## Base Extensions

LimitedSupply, SaleControl

### LimitedSupply

This extension allows to set a maximum number of NFTs that can be minted.

### SaleControl

This extension allows to start and stop sale for NFTs.


## How to move from non-upgradeable to upgradeable extensions

Extensions are not really upgradeable, it follows OpenZeppelin terminology. What this means it that they're ready to be used as proxies.

To move from non-upgradeable to upgradeable extensions, you need to:
- deploy a factory
- deploy an implementation
- deploy an extension using the factory and the implementation

### Example

Let's say you have a non-upgradeable extension that looks like this:

```solidity
contract MintOnlyExtension is NFTExtension {
address public minter;

constructor(address _minter) {
minter = _minter;
}

function mint(address _to, uint256 _tokenId) external {
require(msg.sender == minter, "MintOnlyExtension: not a minter");
_mint(_to, _tokenId);
}
}
```

To move to upgradeable extensions, you need to:

- deploy a factory

```solidity

contract MintOnlyExtensionFactory is NFTExtensionFactory {
function createExtension(
address nft,
// ... other parameters
) public override returns (address extension) {
extension = Clones.clone(_implementation);
extension.initialize(/* ... */);
}
}
```

- create upgradeable version of extension

```solidity
contract MintOnlyExtension is NFTExtension {
address public minter;

constructor() initializer {}

function initialize(address _minter) external initializer {
minter = _minter;
}

function mint(address _to, uint256 _tokenId) external {
require(msg.sender == minter, "MintOnlyExtension: not a minter");
_mint(_to, _tokenId);
}
}
```

- check that the upgradeable version is the same as the non-upgradeable one

```bash
colordiff contracts/extensions/LimitedSupplyMintingExtension.sol contracts/extensions/factories/LimitedSupplyExtension.sol --context=1 | less
```

- deploy the upgradeable version in the factory constructor

```solidity
constructor() {
_implementation = address(new MintOnlyExtension());
}
```
28 changes: 28 additions & 0 deletions contracts/extensions/factories/base/LimitedSupplyUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

abstract contract LimitedSupplyUpgradeable is OwnableUpgradeable {

uint256 public totalMinted;
uint256 public extensionSupply;

function initialize(uint256 _extensionSupply) internal onlyInitializing {
__Ownable_init();

extensionSupply = _extensionSupply;
}

modifier whenLimitedSupplyNotReached(uint256 amount) {
require(
amount + totalMinted <= extensionSupply,
"LimitedSupplyUpgradeable: max extensionSupply reached"
);

totalMinted += amount;

_;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.4;

import "@openzeppelin/contracts/proxy/Clones.sol";

import "./Allowlist.sol";
import "../Allowlist.sol";

contract AllowlistFactory {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/proxy/Clones.sol";

import "../LimitedSupplyExtension.sol";

// contract by buildship.xyz

contract LimitedSupplyExtensionFactory {

event ContractDeployed(
address indexed deployedAddress,
address indexed nft,
address indexed owner,
string title
);

address public immutable implementation;

constructor() {
implementation = address(new LimitedSupplyExtension());
}

function createExtension(
string memory title,
address nft,
uint256 price,
uint256 maxPerMint,
uint256 maxPerWallet,
uint256 extensionSupply,
bool startSale
) external returns (address) {

address payable clone = payable(Clones.clone(implementation));

LimitedSupplyExtension list = LimitedSupplyExtension(clone);

list.initialize(title, nft, price, maxPerMint, maxPerWallet, extensionSupply);

if (startSale) {
list.startSale();
}

list.transferOwnership(msg.sender);

emit ContractDeployed(clone, nft, msg.sender, title);

return clone;

}
}
Loading