Skip to content
This repository was archived by the owner on Oct 8, 2024. It is now read-only.

Commit 96e0327

Browse files
kneelsdevcoindegen
andauthored
Add Dutch invites, list limits, and logic library (#32)
* implement dutch invites * tmp removal of burn to mint (max contract size is being reached * add tests for dutch invite * add negative case for dutch invites * Logic library - Reduce Contract Size (#34) * Revert "tmp removal of burn to mint (max contract size is being reached" This reverts commit e4a1b9b. * move logic to separate library * change librrary parameters to storage to reduce gas * move burnToMint validation logic * update deploy scripts to include library * implement list max supply * add option to reverse burn to mint ratio * fix incorrect max supply check * update test case for list max supply * fix formatting --------- Co-authored-by: PolyDegen <[email protected]> --------- Co-authored-by: PolyDegen <[email protected]>
1 parent 1d51e4b commit 96e0327

File tree

9 files changed

+706
-377
lines changed

9 files changed

+706
-377
lines changed

contracts/Archetype.sol

Lines changed: 84 additions & 271 deletions
Large diffs are not rendered by default.

contracts/ArchetypeLogic.sol

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// SPDX-License-Identifier: MIT
2+
// ArchetypeLogic v0.5.0
3+
//
4+
// d8888 888 888
5+
// d88888 888 888
6+
// d88P888 888 888
7+
// d88P 888 888d888 .d8888b 88888b. .d88b. 888888 888 888 88888b. .d88b.
8+
// d88P 888 888P" d88P" 888 "88b d8P Y8b 888 888 888 888 "88b d8P Y8b
9+
// d88P 888 888 888 888 888 88888888 888 888 888 888 888 88888888
10+
// d8888888888 888 Y88b. 888 888 Y8b. Y88b. Y88b 888 888 d88P Y8b.
11+
// d88P 888 888 "Y8888P 888 888 "Y8888 "Y888 "Y88888 88888P" "Y8888
12+
// 888 888
13+
// Y8b d88P 888
14+
// "Y88P" 888
15+
16+
pragma solidity ^0.8.4;
17+
18+
import "erc721a-upgradeable/contracts/ERC721AUpgradeable.sol";
19+
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
20+
import "solady/src/utils/MerkleProofLib.sol";
21+
import "solady/src/utils/ECDSA.sol";
22+
23+
error InvalidConfig();
24+
error MintNotYetStarted();
25+
error WalletUnauthorizedToMint();
26+
error InsufficientEthSent();
27+
error ExcessiveEthSent();
28+
error Erc20BalanceTooLow();
29+
error MaxSupplyExceeded();
30+
error ListMaxSupplyExceeded();
31+
error NumberOfMintsExceeded();
32+
error MintingPaused();
33+
error InvalidReferral();
34+
error InvalidSignature();
35+
error BalanceEmpty();
36+
error TransferFailed();
37+
error MaxBatchSizeExceeded();
38+
error BurnToMintDisabled();
39+
error NotTokenOwner();
40+
error NotPlatform();
41+
error NotApprovedToTransfer();
42+
error InvalidAmountOfTokens();
43+
error WrongPassword();
44+
error LockedForever();
45+
46+
//
47+
// STRUCTS
48+
//
49+
struct Auth {
50+
bytes32 key;
51+
bytes32[] proof;
52+
}
53+
54+
struct MintTier {
55+
uint16 numMints;
56+
uint16 mintDiscount; //BPS
57+
}
58+
59+
struct Discount {
60+
uint16 affiliateDiscount; //BPS
61+
MintTier[] mintTiers;
62+
}
63+
64+
struct Config {
65+
string baseUri;
66+
address affiliateSigner;
67+
address ownerAltPayout; // optional alternative address for owner withdrawals.
68+
address superAffiliatePayout; // optional super affiliate address, will receive half of platform fee if set.
69+
uint32 maxSupply;
70+
uint32 maxBatchSize;
71+
uint16 affiliateFee; //BPS
72+
uint16 platformFee; //BPS
73+
uint16 defaultRoyalty; //BPS
74+
Discount discounts;
75+
}
76+
77+
struct Options {
78+
bool uriLocked;
79+
bool maxSupplyLocked;
80+
bool affiliateFeeLocked;
81+
bool discountsLocked;
82+
bool ownerAltPayoutLocked;
83+
bool royaltyEnforcementEnabled;
84+
bool royaltyEnforcementLocked;
85+
bool provenanceHashLocked;
86+
}
87+
88+
struct DutchInvite {
89+
uint128 price;
90+
uint128 reservePrice;
91+
uint128 delta;
92+
uint32 start;
93+
uint32 limit;
94+
uint32 maxSupply;
95+
uint32 interval;
96+
address tokenAddress;
97+
}
98+
99+
struct Invite {
100+
uint128 price;
101+
uint32 start;
102+
uint32 limit;
103+
uint32 maxSupply;
104+
address tokenAddress;
105+
}
106+
107+
struct OwnerBalance {
108+
uint128 owner;
109+
uint128 platform;
110+
}
111+
112+
struct BurnConfig {
113+
IERC721AUpgradeable archetype;
114+
bool enabled;
115+
bool reversed; // side of the ratio (false=burn {ratio} get 1, true=burn 1 get {ratio})
116+
uint16 ratio;
117+
uint64 start;
118+
uint64 limit;
119+
}
120+
121+
address constant PLATFORM = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; // TEST (account[2])
122+
// address private constant PLATFORM = 0x86B82972282Dd22348374bC63fd21620F7ED847B;
123+
uint16 constant MAXBPS = 5000; // max fee or discount is 50%
124+
125+
library ArchetypeLogic {
126+
// calculate price based on affiliate usage and mint discounts
127+
function computePrice(
128+
DutchInvite storage invite,
129+
Discount storage discounts,
130+
uint256 numTokens,
131+
bool affiliateUsed
132+
) public view returns (uint256) {
133+
uint256 price = invite.price;
134+
if (invite.interval != 0) {
135+
uint256 diff = (((block.timestamp - invite.start) / invite.interval) * invite.delta);
136+
if (price > invite.reservePrice) {
137+
if (diff > price - invite.reservePrice) {
138+
price = invite.reservePrice;
139+
} else {
140+
price = price - diff;
141+
}
142+
} else if (price < invite.reservePrice) {
143+
if (diff > invite.reservePrice - price) {
144+
price = invite.reservePrice;
145+
} else {
146+
price = price + diff;
147+
}
148+
}
149+
}
150+
151+
uint256 cost = price * numTokens;
152+
153+
if (affiliateUsed) {
154+
cost = cost - ((cost * discounts.affiliateDiscount) / 10000);
155+
}
156+
157+
for (uint256 i = 0; i < discounts.mintTiers.length; i++) {
158+
if (numTokens >= discounts.mintTiers[i].numMints) {
159+
return cost = cost - ((cost * discounts.mintTiers[i].mintDiscount) / 10000);
160+
}
161+
}
162+
return cost;
163+
}
164+
165+
function validateMint(
166+
DutchInvite storage i,
167+
Config storage config,
168+
Auth calldata auth,
169+
uint256 quantity,
170+
address owner,
171+
address affiliate,
172+
uint256 curSupply,
173+
mapping(address => mapping(bytes32 => uint256)) storage minted,
174+
mapping(bytes32 => uint256) storage listSupply,
175+
bytes calldata signature
176+
) public view {
177+
if (affiliate != address(0)) {
178+
if (affiliate == PLATFORM || affiliate == owner || affiliate == msg.sender) {
179+
revert InvalidReferral();
180+
}
181+
validateAffiliate(affiliate, signature, config.affiliateSigner);
182+
}
183+
184+
if (i.limit == 0) {
185+
revert MintingPaused();
186+
}
187+
188+
if (!verify(auth, i.tokenAddress, msg.sender)) {
189+
revert WalletUnauthorizedToMint();
190+
}
191+
192+
if (block.timestamp < i.start) {
193+
revert MintNotYetStarted();
194+
}
195+
196+
if (i.limit < i.maxSupply) {
197+
uint256 totalAfterMint = minted[msg.sender][auth.key] + quantity;
198+
199+
if (totalAfterMint > i.limit) {
200+
revert NumberOfMintsExceeded();
201+
}
202+
}
203+
204+
if (i.maxSupply < config.maxSupply) {
205+
uint256 totalAfterMint = listSupply[auth.key] + quantity;
206+
if (totalAfterMint > i.maxSupply) {
207+
revert ListMaxSupplyExceeded();
208+
}
209+
}
210+
211+
if (quantity > config.maxBatchSize) {
212+
revert MaxBatchSizeExceeded();
213+
}
214+
215+
if ((curSupply + quantity) > config.maxSupply) {
216+
revert MaxSupplyExceeded();
217+
}
218+
219+
uint256 cost = computePrice(i, config.discounts, quantity, affiliate != address(0));
220+
221+
if (i.tokenAddress != address(0)) {
222+
IERC20Upgradeable erc20Token = IERC20Upgradeable(i.tokenAddress);
223+
if (erc20Token.allowance(msg.sender, address(this)) < cost) {
224+
revert NotApprovedToTransfer();
225+
}
226+
227+
if (erc20Token.balanceOf(msg.sender) < cost) {
228+
revert Erc20BalanceTooLow();
229+
}
230+
231+
if (msg.value != 0) {
232+
revert ExcessiveEthSent();
233+
}
234+
} else {
235+
if (msg.value < cost) {
236+
revert InsufficientEthSent();
237+
}
238+
239+
if (msg.value > cost) {
240+
revert ExcessiveEthSent();
241+
}
242+
}
243+
}
244+
245+
function validateBurnToMint(
246+
Config storage config,
247+
BurnConfig storage burnConfig,
248+
uint256[] calldata tokenIds,
249+
uint256 curSupply,
250+
mapping(address => mapping(bytes32 => uint256)) storage minted
251+
) public view {
252+
if (!burnConfig.enabled) {
253+
revert BurnToMintDisabled();
254+
}
255+
256+
if (block.timestamp < burnConfig.start) {
257+
revert MintNotYetStarted();
258+
}
259+
260+
// check if msg.sender owns tokens and has correct approvals
261+
for (uint256 i = 0; i < tokenIds.length; i++) {
262+
if (burnConfig.archetype.ownerOf(tokenIds[i]) != msg.sender) {
263+
revert NotTokenOwner();
264+
}
265+
}
266+
267+
if (!burnConfig.archetype.isApprovedForAll(msg.sender, address(this))) {
268+
revert NotApprovedToTransfer();
269+
}
270+
271+
uint256 quantity;
272+
if (burnConfig.reversed) {
273+
quantity = tokenIds.length * burnConfig.ratio;
274+
} else {
275+
if (tokenIds.length % burnConfig.ratio != 0) {
276+
revert InvalidAmountOfTokens();
277+
}
278+
quantity = tokenIds.length / burnConfig.ratio;
279+
}
280+
281+
if (quantity > config.maxBatchSize) {
282+
revert MaxBatchSizeExceeded();
283+
}
284+
285+
if (burnConfig.limit < config.maxSupply) {
286+
uint256 totalAfterMint = minted[msg.sender][bytes32("burn")] + quantity;
287+
288+
if (totalAfterMint > burnConfig.limit) {
289+
revert NumberOfMintsExceeded();
290+
}
291+
}
292+
293+
if ((curSupply + quantity) > config.maxSupply) {
294+
revert MaxSupplyExceeded();
295+
}
296+
}
297+
298+
function validateAffiliate(
299+
address affiliate,
300+
bytes calldata signature,
301+
address affiliateSigner
302+
) public view {
303+
bytes32 signedMessagehash = ECDSA.toEthSignedMessageHash(
304+
keccak256(abi.encodePacked(affiliate))
305+
);
306+
address signer = ECDSA.recover(signedMessagehash, signature);
307+
308+
if (signer != affiliateSigner) {
309+
revert InvalidSignature();
310+
}
311+
}
312+
313+
function verify(
314+
Auth calldata auth,
315+
address tokenAddress,
316+
address account
317+
) public pure returns (bool) {
318+
if (auth.key == "" || auth.key == keccak256(abi.encodePacked(tokenAddress))) {
319+
return true;
320+
}
321+
322+
return MerkleProofLib.verify(auth.proof, auth.key, keccak256(abi.encodePacked(account)));
323+
}
324+
}

contracts/Factory.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
pragma solidity ^0.8.4;
1717

1818
import "./Archetype.sol";
19+
import "./ArchetypeLogic.sol";
1920
import "@openzeppelin/contracts-upgradeable/proxy/ClonesUpgradeable.sol";
2021
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
2122

@@ -33,7 +34,7 @@ contract Factory is OwnableUpgradeable {
3334
address _receiver,
3435
string memory name,
3536
string memory symbol,
36-
Archetype.Config calldata config
37+
Config calldata config
3738
) external payable returns (address) {
3839
address clone = ClonesUpgradeable.clone(archetype);
3940
Archetype token = Archetype(clone);

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
"test": "hardhat test test/archetype.test",
88
"test:specific": "hardhat test test/archetype.test --grep",
99
"start": "concurrently \"cd api && npm start\" \"cd app && npm run dev\"",
10-
"deploy": "npx hardhat run scripts/deployRemilia.ts --network localhost",
10+
"deploy": "npx hardhat run scripts/deployArchetype.ts --network localhost",
1111
"network": "npx hardhat node",
12-
"verify": "npx hardhat verify 0xc078Aed674c230cDD7ADD602E077797d08d8f0c2 --network rinkeby"
12+
"verify": "npx hardhat verify 0xc078Aed674c230cDD7ADD602E077797d08d8f0c2 --network goerli"
1313
},
1414
"repository": {
1515
"type": "git",
16-
"url": "git+https://github.com/addy-the-young/scatter.git"
16+
"url": "git+https://github.com/scatter-art/contracts.git"
1717
},
1818
"author": "",
1919
"license": "ISC",
2020
"bugs": {
21-
"url": "https://github.com/addy-the-young/scatter/issues"
21+
"url": "https://github.com/scatter-art/contracts/issues"
2222
},
23-
"homepage": "https://github.com/addy-the-young/scatter#readme",
23+
"homepage": "https://github.com/scatter-art/contracts#readme",
2424
"dependencies": {
2525
"concurrently": "6.4.0",
2626
"ethereumjs-util": "7.1.4",

scripts/deployArchetype.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { ethers, run } from "hardhat";
33
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
44

55
async function main() {
6-
const Archetype = await ethers.getContractFactory("Archetype");
6+
const ArchetypeLogic = await ethers.getContractFactory("ArchetypeLogic");
7+
const archetypeLogic = await ArchetypeLogic.deploy();
8+
const Archetype = await ethers.getContractFactory("Archetype", {
9+
libraries: {
10+
ArchetypeLogic: archetypeLogic.address,
11+
},
12+
});
713

814
const archetype = await Archetype.deploy();
915

scripts/deployFactory.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { ethers, upgrades, run } from "hardhat";
33
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
44

55
async function main() {
6-
const Archetype = await ethers.getContractFactory("Archetype");
6+
const ArchetypeLogic = await ethers.getContractFactory("ArchetypeLogic");
7+
const archetypeLogic = await ArchetypeLogic.deploy();
8+
const Archetype = await ethers.getContractFactory("Archetype", {
9+
libraries: {
10+
ArchetypeLogic: archetypeLogic.address,
11+
},
12+
});
713

814
const archetype = await Archetype.deploy();
915

0 commit comments

Comments
 (0)