This repository has been archived by the owner on Sep 29, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
djviau
committed
Sep 22, 2023
1 parent
79a5785
commit 9fd8d3c
Showing
23 changed files
with
2,091 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# Changing the behavior of your NFT | ||
|
||
Solidity supports inheritance, so instead of tampering with the NFT contracts that come with Shipyard directly, you'll create a new contract and have it inherit from one of the existing contracts. See [Solidity by Example](https://solidity-by-example.org/inheritance/) and [GeeksForGeeks](https://www.geeksforgeeks.org/solidity-inheritance/) for more info on inheritance. The practical takeway is: | ||
|
||
- Make a new file in `src/` called `MyVeryOwnNFT.sol` | ||
- Import `shipyard-core/reference/AbstractNFT.sol:` and inherit from it (`contract MyVeryOwnNFT is AbstractNFT { ...`) | ||
- Override some functions (`name`, `symbol`, and `tokenURI` are good starting places, e.g. `function name() public pure override returns (string memory) { ...`) | ||
- Add some other fun stuff if you want to | ||
|
||
And you'll end up with something that might look like [Dockmaster](../../src/Dockmaster.sol), but with your own special touches. | ||
|
||
(You'll notice that Dockmaster itself looks a lot like [shipyard-core's ExampleNFT.sol](https://github.com/ProjectOpenSea/shipyard-core/blob/main/src/reference/ExampleNFT.sol), but it's special to me now, because I've made changes!) | ||
|
||
What's great about this inheritance pattern is that the `ERC721ConduitPreapproved_Solady` we inheritied via `AbstractNFT` has already handled all the routine ERC721 functionality for us in an almost comically optimized way. So we can burn calories on making something cool, instead of reinventing the nonfungible token wheel. But when someone calls the `ownerOf` function on my contract, it's going to work as expected. | ||
|
||
And because I inherited `AbstractNFT`, which itself inherit `OnchainTraits`, I automatically get functionality that lets me manage on chain traits. So, now I want to add the following trait to token ID 1: | ||
|
||
``` | ||
{ | ||
"trait_type": "Dock Material" | ||
"value": "Aluminum" | ||
} | ||
``` | ||
|
||
To accomplish that, I'll need to first set a trait label by calling `setTraitLabel` with `bytes32(uint256(0x6d6174657269616c))` (the bytes encoded version of "material") as the `traitKey` and the following struct as the `TraitLabel` arg: | ||
|
||
``` | ||
TraitLabel memory myFirstDynamicTrait {( | ||
fullTraitKey: myFirstTraitKeyString, | ||
traitLabel: myFirstLabelString, | ||
acceptableValues: myFirstArrayOfAcceptableValuesStrings, | ||
fullTraitValues: myFirstArrayOfFullTraitValues, | ||
displayType: myFirstDisplayTypeEnumValue, | ||
editors: myFirstEditorsValue; | ||
)} | ||
``` | ||
|
||
Look at `TraitLabelLib.sol` to get a better sense of how to set those values up. | ||
|
||
Then I just call `setTrait` with `bytes32(uint256(0x6d6174657269616c))` as the `traitKey` argument, `1` as the `tokenID` argument, and `bytes32(uint256(<bytes32_value_of_my_trait>))` as the `trait` argument. The `_setTrait` function will store the value (`_traits[tokenId][traitKey] = value;`) and emit a `TraitUpdated` event, to put the world on notice that token ID 1 now has a new trait on it. | ||
|
||
Now is the time to go wild. Make some wild generative art. Create a novel minting mechanic (only contract deployers are eligible?!). Build an AMM into it. Whatever idea brought you here in the first place, here's where you wire it up. Either bolt it on (like `SayHiToMom`) or override existing functionality (like `tokenURI`). Your NFT is as interesting as you make it. | ||
|
||
## Next up: | ||
|
||
[Deploying for real](Deploying.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Deploying for real | ||
|
||
OK, here's the big moment. Now that you've [gotten your head into ERC721s](ERC721Concepts.md), [set up your environment](EnvironmentSetup.md), [ran some tests](Testing.md), [made your custom contract](CustomNFTFunctionality.md), ideally tested its exciting new functionality, and now it's time to deploy. The process is basically identical to the example from earlier. You'll just need to run a command like this, but with your contract swapped in for Dockmaster. | ||
|
||
``` | ||
forge create --rpc-url $ETH_RPC_URL \ | ||
--private-key $MY_ACTUAL_PK_BE_CAREFUL \ | ||
--etherscan-api-key $ETHERSCAN_API_KEY \ | ||
--verify \ | ||
src/Dockmaster.sol:Dockmaster \ | ||
--constructor-args "Dockmaster NFT" "DM" | ||
``` | ||
|
||
Forge compiles (or skips compiling bc you've got a clean build from all those tests you ran, right? right?), logs the address you deployed from, shows the transaction hash of the deployment transaction, and then automatically verifies the contract on Etherscan. It's definitely worth configuring your Etherscan API key and getting verification over with at this phase. It's way easier to do in Forge than in the Etherscan UI. | ||
|
||
And that's it! | ||
|
||
## Optional bonus step: deploying to a hip, gas-efficient address | ||
|
||
Ever notice how Seaport is deployed to [an address](https://etherscan.io/address/0x00000000000000adc04c56bf30ac9d3c0aaf14dc) that starts with a bunch of `0`s? Ever wonder how that works? Ever wonder why? | ||
|
||
The short version of "why?" is simple: gas efficiency. The technical nuance of "why?" is meatier, but the heart of it lies in the fact that Ethereum charges you less for schlepping a zero around than for a non-zero. Check out [this article by 0age](https://medium.com/coinmonks/on-efficient-ethereum-addresses-3fef0596e263) and [this article by 0xfoobar](https://0xfoobar.substack.com/p/vanity-addresses) if your interest is piqued. | ||
|
||
So, want to deploy your own contract to a cool address instead of just taking what you get? Fortunately, it's pretty straightforward. | ||
|
||
### Get create2crunch | ||
|
||
We'll be using [create2crunch](https://github.com/0age/create2crunch) to "mine" a vanity address. Go to [https://github.com/0age/create2crunch](https://github.com/0age/create2crunch), read the docs, and clone the repo. | ||
|
||
### Get set up | ||
|
||
First, `cd` into your local create2crunch repo. Then, you'll need to set up your environment variables. The create2crunch repo recommends doing this from the comand line, but I recommend setting up a .env file. Initially, it should looks like this: | ||
|
||
``` | ||
export FACTORY="0x0000000000FFe8B47B3e2130213B802212439497" | ||
export CALLER="<your_deployer_address>" | ||
export INIT_CODE="" | ||
export INIT_CODE_HASH="" | ||
``` | ||
|
||
Now, we need to generate the value for that `INIT_CODE_HASH` variable, which will be a three step process: | ||
|
||
- Refresh your `out/` directory | ||
- Find the contract's deployment bytecode | ||
- Hash it | ||
|
||
First, clear out the contents of your `out` directory by running `rm -rf out/*`. Then run `forge clean && forge build` to repopulate it. | ||
|
||
Next, we'll find the bytecode buried deep in a big, dense file in our NFT directory. For me, the command to run is `cat out/Dockmaster.sol/Dockmaster.json`. For you, it'll be analogous, but with your NFT contract name swapped in. Then I `cmd` + `f` for `"bytecode"` (note: make sure you're getting the bytecode used for deployment and not the `deployedBytecode`, which lacks constructor arguments, if they exist). We want to grab the whole massive `bytecode` hex string and set it as the value of our `INIT_CODE` variable. | ||
|
||
Then, source the `.env` file and run `cast keccak $INIT_CODE`. It should print a 32 byte value in response. Set that as the value of `INIT_CODE_HASH`, and then source your .env again. | ||
|
||
### Mining | ||
|
||
Now that everything's configured, mining is as simple as running `cargo run --release $FACTORY $CALLER $INIT_CODE_HASH 2 2 4` and waiting. You should get a hit almost instantly, since that command accepts addresses that have 2 leading or 4 total zeroes. | ||
|
||
As soon as you get a hit, take it over to [the create2 factory](https://etherscan.io/address/0x0000000000FFe8B47B3e2130213B802212439497) and check that it's working. Call the `findCreate2Address` (not `findCreate2AddressViaHash`) with the `salt` provided by create2crunch. The output from create2crunch will look like this: | ||
|
||
``` | ||
0x22...26 => 0x0000F5f864d1cc53dC66efE16B98ceeC2c497695 => 0 (2 / 2) | ||
0x22...ff => 0x0000aE343783fcDF5f8Fc7d00C5b082136177048 => 0 (2 / 2) | ||
0x22...4c => 0x00008E5917BDa2fd65cBF0E1705403f1bd5C512C => 0 (2 / 2) | ||
0x22...26 => 0x0000389A4A66fD7AA80221b8D193ae6B478c4c17 => 0 (2 / 2) | ||
``` | ||
|
||
Paste one of those 32 byte salts from the left column into the `salt` field and the full `$INIT_CODE` value into the `initCode` field, then click the "Query" button. If things are working as expected, when you paste in the `0x22...ff` salt, you'll get the `0x0000aE343783fcDF5f8Fc7d00C5b082136177048` address as a response. If you get some other address, you need to double check all your values and configuration end to end. | ||
|
||
Once you're reasonably confident that everything is working as expected, you can run the command with jacked up expectations: `cargo run --release $FACTORY $CALLER $INIT_CODE_HASH 2 4 6`. You might have to wait longer, but you'll get better results. | ||
|
||
Remember that if you make any changes once you're sitting around waiting to for a super cool address, you'll need to reset your `$INIT_CODE` and `$INIT_CODE_HASH` values. | ||
|
||
### Deploying like a cool kid | ||
|
||
Once you've got a salt that produces a deploy address you're happy with, deploying is as simple as going to [the "Write Contract" tab](https://etherscan.io/address/0x0000000000FFe8B47B3e2130213B802212439497#writeContract), and calling the `safeCreate2` function with your preferred salt and `initCode`. You don't have to enter anything in the top field (unless you decided to do payable constructor for some reason). | ||
|
||
Since you're fluent with the Foundry toolset now, you could also use [`cast send`](https://book.getfoundry.sh/reference/cast/cast-send?highlight=cast%20send#cast-send) to send the transaction from the command line: | ||
|
||
``` | ||
cast send 0x0000000000FFe8B47B3e2130213B802212439497 "safeCreate2(bytes32, bytes)" 0x... 0x... | ||
``` | ||
|
||
Finally, verify your contract [on Etherscan](https://etherscan.io/verifyContract) or [using Forge](https://book.getfoundry.sh/forge/deploying?highlight=verify#verifying-a-pre-existing-contract). | ||
|
||
To be clear, this is mostly about the cool factor. But it also gives you gas efficiency benefits, sceurity benefits, cross-chain consistency, and more. And since you know the address before you deploy, you can code it into your frontend, etc. before you've revealed it to the rest of the world. | ||
|
||
## Back to the table of contents | ||
|
||
[Take it from the top](Overview.md) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Getting your head into the code | ||
|
||
Feel free to skip this section if you've already read about ERC-721 or perused the code on your own. | ||
|
||
There's nothing like going straight to the source: [https://eips.ethereum.org/EIPS/eip-721#specification](https://eips.ethereum.org/EIPS/eip-721#specification). The ERC-721 spec outlines the bare minimum interface and behavior that a contract needs to implement in order to be recognized and treated as an ERC-721 contract by the rest of the web 3 ecosystem (such as OpenSea, block explorers, etc.). There are only a half dozen operative "MUST"s in there, so we've got a lot of leeway. Eventually, we're going to deploy a snazzy, gas-optimized ERC721 with some bonus features. But for now, let's take a look at the stock version, [OpenZeppelin's example ERC721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol) | ||
|
||
TODO (blocked): update with links within the repo throughout. | ||
|
||
OpenZeppelin's contracts show a straightforward, battle-tested, minimal implementation of the ERC-721 spec. For example, the ERC-721 spec states that `safeTransferFrom` "Throws if `_from` is not the current owner." And we can see that the required functionality is implemented on [lines 148-150](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L148-L150) of OpenZeppelin's ERC721: | ||
|
||
``` | ||
if (previousOwner != from) { | ||
revert ERC721IncorrectOwner(from, tokenId, previousOwner); | ||
} | ||
``` | ||
|
||
Take a pass through the spec and identify in the OZ code where each requirement is met. It's elegant, simple, and ingenious. NFTs are great! | ||
|
||
But we can build something even better. | ||
|
||
[EVM](https://ethereum.org/en/developers/docs/evm/) based blockchains use the concept of [gas](https://ethereum.org/en/developers/docs/gas/) to address spam and infinite loops. This means that every operation on chain costs real money. So, by making changes at the level of implementation details in your smart contract, you can save your users real money. A penny here and a buck there add up to a lot! | ||
|
||
Compare [Solady's `_ownerOf`](https://github.com/Vectorized/solady/blob/main/src/tokens/ERC721.sol#L369-L378) with [OpenZeppelin's](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L168-L178). OZ's relies on the functionality that comes for free with solidity, which is convenient, but less gas efficient. Solady's implementation uses assembly to trim the gas costs down, but it comes at the expense of readability. | ||
|
||
But don't get nervous! This is just a quick tour through the code we're going to be working on top of. You'll be able to make a custom, interesting NFT with maximally optimized guts without having to get elbows deep in `mstore`s. You won't even have to twiddle any bits by hand unless you want to. | ||
|
||
Let's get to it. | ||
|
||
## Next up: | ||
|
||
[Setting up your environment](EnvironmentSetup.md) |
Oops, something went wrong.