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

Dan/2023/08/document deploy process #18

Closed
wants to merge 7 commits into from
Closed
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/seaport-types"]
path = lib/seaport-types
url = https://github.com/projectopensea/seaport-types
[submodule "lib/shipyard-core"]
path = lib/shipyard-core
url = https://github.com/projectopensea/shipyard-core
86 changes: 81 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Shipyard comes with some batteries included

- [OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts), [Solady](https://github.com/Vectorized/solady), and Shipyard-core smart contracts as dependencies, ready with [`solc` remappings](https://docs.soliditylang.org/en/latest/path-resolution.html#import-remapping) so you can jump into writing your own contracts right away
- `forge fmt` configured as the default formatter for VSCode projects
- Github Actions workflows that run `forge fmt --check` and `forge test` on every push and PR
- GitHub Actions workflows that run `forge fmt --check` and `forge test` on every push and PR
- A separate action to automatically fix formatting issues on PRs by commenting `!fix` on the PR
- A pre-configured, but still minimal `foundry.toml`
- high optimizer settings by default for gas-efficient smart contracts
Expand All @@ -39,6 +39,76 @@ Shipyard comes with some batteries included

## Usage

Shipyard can be used as a starting point or a toolkit in a wide variety of circumstances. In general, if you're building something NFT related, you're likely to find something useful here. For the sake of exploring some of what Shipyard has to offer in concrete terms, here's a guide on how to deploy an NFT contract.

### Quick Deploy Guide

To deploy an NFT contract to the Goerli testnet, fund an address with 0.25 Goerli ETH, swap in the appropriate values for `<your_key>` and `<your_pk>` in this command, open a terminal window, and run the following:

TODO: update this to do the template thing. And add a bit about staying up to date with the latest.

```
mkdir my-shipyard-based-project &&
cd my-shipyard-based-project &&
curl -L https://foundry.paradigm.xyz | bash &&
foundryup &&
forge init --template projectopensea/shipyard &&
forge build &&
export GOERLI_RPC_URL='https://goerli.blockpi.network/v1/rpc/public &&
export MY_ACTUAL_PK_BE_CAREFUL='<your_pk>' &&
forge create --rpc-url $GOERLI_RPC_URL \
--private-key $MY_ACTUAL_PK_BE_CAREFUL \
lib/shipyard-core/src/reference/ExampleNFT.sol:ExampleNFT \
--constructor-args "Tutorial Example NFT" "TENFT"
```

A quick breakdown of each step follows.

Create a directory, `cd` into it, :
```
mkdir my-shipyard-based-project &&
cd my-shipyard-based-project &&
curl -L https://foundry.paradigm.xyz | bash
```

Install the `foundryup` up command and run it, which in turn installs forge, cast, anvil, and chisel:
```
curl -L https://foundry.paradigm.xyz | bash &&
foundryup
```

Create a new Foundry project based on Shipyard, which also initializes a new git repository.
```
forge init --template projectopensea/shipyard
```

Install dependencies and compile the contracts:
```
forge build
```

Set up your environment variables:
```
export GOERLI_RPC_URL='https://goerli.blockpi.network/v1/rpc/public &&
export MY_ACTUAL_PK_BE_CAREFUL='<your_pk>'
```

Run the `forge create` command, which deploys the contract:
```
forge create --rpc-url $GOERLI_RPC_URL \
--private-key $MY_ACTUAL_PK_BE_CAREFUL \
lib/shipyard-core/src/reference/ExampleNFT.sol:ExampleNFT \
--constructor-args "Tutorial Example NFT" "TENFT"
```

See https://book.getfoundry.sh/reference/forge/forge-create for more information on `forge create`.

Running this command deploys the example NFT contract, but it's a good way to check for a properly functioning dev environment. Deploying to mainnet instead of Goerli just requires using a mainnet RPC URL instead of a Goerli RPC URL.

### Custom contract deployment tutorial

See [the full tutorial](shipyardDocs/exampleNFTTutorial/Overview.md) for more detail on modifying the example contract, writing tests, deploying, and more.

### Reinitialize Submodules
When working across branches with different dependencies, submodules may need to be reinitialized. Run
```bash
Expand All @@ -51,13 +121,19 @@ Run
./coverage-report
```

### Useful Aliases

```
alias gm="foundryup"
alias ff="forge fmt"
```


## Roadmap

- [x] Configure test.yml to run `forge test` on every push to main and PR
- [x] Add a `forge fmt --check` workflow to the Github Actions
- [x] Add an optional `forge fmt` fix workflow to the Github Actions
- [x] Add a `forge fmt --check` workflow to the GitHub Actions
- [x] Add an optional `forge fmt` fix workflow to the GitHub Actions
- [ ] Include base dependencies
- [x] OZ
- [ ] Pin to version
Expand All @@ -73,7 +149,7 @@ Run

Copilot suggests:
- [ ] Additional github actions
- [ ] Add a `forge deploy` workflow to the Github Actions
- [ ] Add a `forge verify` workflow to the Github Actions
- [ ] Add a `forge deploy` workflow to the GitHub Actions
- [ ] Add a `forge verify` workflow to the GitHub Actions
- [ ] Add a `forge verify` script to the top-level helpers
- [ ]
41 changes: 41 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ fs_permissions = [{ access = "read", path = "./via_ir-out" }]
# bytecode is typically unaffected past ~1 million runs anyway
optimizer_runs = 99_999_999

[rpc_endpoints]
goerli = "${GOERLI_RPC_URL}"

# via_ir pipeline is very slow - use a separate profile to pre-compile and then use vm.getCode to deploy
[profile.via_ir]
via_ir = true
Expand All @@ -35,5 +38,43 @@ out = 'via_ir-out'
[profile.CI.fuzz]
fuzz_runs = 1024

[profile.ffi]
ffi = true
test = 'test-ffi'
fs_permissions = [{ access = 'read-write', path = './test-ffi/' }]

# Use a very small number of default fuzz runs to speed up local testing. It is
# recommended to increase this number or use the `--fuzz-runs=256` flag to run
# a proper fuzzing campaign before yeeting your project to mainnet.
[profile.ffi.fuzz]
runs = 5

[profile.ci-ffi]
ffi = true
test = 'test-ffi'
fs_permissions = [{ access = 'read-write', path = './test-ffi/' }]

# Use a relatively small number of fuzz runs in CI because the ffi lifecycle is
# slow.
[profile.ci-ffi.fuzz]
runs = 10

[profile.tutorial]
src = "lib/shipyard-core/src"
test = 'lib/shipyard-core/test'
out = "lib/shipyard-core/out"
libs = ["lib/shipyard-core/lib"]
remappings = [
'src/=lib/shipyard-core/src/',
'dynamic-traits/=lib/shipyard-core/lib/dynamic-traits/src/',
'forge-std/=lib/shipyard-core/lib/forge-std/src',
'openzeppelin-contracts/=lib/shipyard-core/lib/openzeppelin-contracts/contracts/',
'openzeppelin-contracts/contracts/=lib/shipyard-core/lib/openzeppelin-contracts/contracts/',
'seaport-types/=lib/shipyard-core/lib/seaport-types/src/',
'shipyard-core/=lib/shipyard-core/src/',
'solady-test/=lib/shipyard-core/lib/solady/test/',
'solady/=lib/shipyard-core/lib/solady/src/',
'solarray/=lib/shipyard-core/lib/solarray/src/',
]

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
1 change: 1 addition & 0 deletions lib/shipyard-core
Submodule shipyard-core added at 76fab0
30 changes: 29 additions & 1 deletion sample.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
## TESTING STUFF

# Use this profile to run the shipyard-core tests from the shipyard directory.
export FOUNDRY_PROFILE='tutorial'

# see https://book.getfoundry.sh/reference/config/testing#verbosity
FOUNDRY_VERBOSITY=3
export FOUNDRY_VERBOSITY=3

# see https://book.getfoundry.sh/reference/config/testing?highlight=FOUNDRY_FUZZ_RUNS#fuzz
export FOUNDRY_FUZZ_RUNS=128


## COMPILER STUFF

# see https://book.getfoundry.sh/reference/config/solidity-compiler?highlight=via_ir#optimizer,
# https://book.getfoundry.sh/reference/config/overview?highlight=foundry_profile#profiles,
# and the foundry.toml file. You can uncomment this to enable compiling with the optimizer.
# export FOUNDRY_PROFILE=via_ir


## DEPLOYING STUFF

# Note: emo.eth is working on getting keystores wired up, which will obviate the
# need for handling a private key directly. Stay tuned.
export MY_ACTUAL_PK_BE_CAREFUL=0

export GOERLI_RPC_URL='https://goerli.infura.io/v3/<your_key>'
export ETH_RPC_URL='https://mainnet.infura.io/v3/<your_key>'

export ETHERSCAN_API_KEY='<your_key>'
17 changes: 17 additions & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import "forge-std/Script.sol";

import {ExampleNFT} from "shipyard-core/reference/ExampleNFT.sol";

import "forge-std/console.sol";

contract Deploy is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("MY_ACTUAL_PK_BE_CAREFUL");
vm.startBroadcast(deployerPrivateKey);
new ExampleNFT('Example', 'EXNFT');
vm.stopBroadcast();
}
}
155 changes: 155 additions & 0 deletions shipyardDocs/exampleNFTTutorial/CustomNFTFunctionality.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 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 `Dockmaster.sol`
- Import `shipyard-core/reference/AbstractNFT.sol:` and inherit from it (`contract Dockmaster 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 this:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {json} from "shipyard-core/onchain/json.sol";
import {svg} from "shipyard-core/onchain/svg.sol";
import {LibString} from "solady/utils/LibString.sol";
import {Base64} from "solady/utils/Base64.sol";
import {Solarray} from "solarray/Solarray.sol";
import {Metadata, DisplayType} from "shipyard-core/onchain/Metadata.sol";
import {AbstractNFT} from "shipyard-core/reference/AbstractNFT.sol";

contract Dockmaster is AbstractNFT {
using LibString for string;
using LibString for uint256;

uint256 currentId;

event SayHiToMom(string message);

constructor() {
emit SayHiToMom("Hi Mom!");
}

function name() public pure override returns (string memory) {
return "Dockmaster";
}

function symbol() public pure override returns (string memory) {
return "DOCK";
}

function tokenURI(uint256 tokenId) public view override returns (string memory) {
return Metadata.base64JsonDataURI(stringURI(tokenId));
}

function stringURI(uint256 tokenId) internal view returns (string memory) {
return json.objectOf(
Solarray.strings(
json.property("name", string.concat("Dockmaster NFT #", tokenId.toString())),
json.property("description", string.concat("My dock has ", tokenId.toString(), " slips.")),
json.property("image", Metadata.svgDataURI(image(tokenId))),
_attribute(tokenId)
)
);
}

function _attribute(uint256 tokenId) internal view returns (string memory) {
string[] memory staticTraits = Solarray.strings(
Metadata.attribute({
traitType: "Slip Number",
value: tokenId.toString(),
displayType: DisplayType.Number
}),
Metadata.attribute({traitType: "Dock Side", value: tokenId % 2 == 0 ? "North" : "South"})
);
string[] memory dynamicTraits = _dynamicAttributes(tokenId);
string[] memory combined = new string[](staticTraits.length + dynamicTraits.length);
for (uint256 i = 0; i < staticTraits.length; i++) {
combined[i] = staticTraits[i];
}
for (uint256 i = 0; i < dynamicTraits.length; i++) {
combined[staticTraits.length + i] = dynamicTraits[i];
}
return json.rawProperty("attributes", json.arrayOf(combined));
}

function image(uint256 tokenId) internal pure returns (string memory) {
return svg.top({
props: string.concat(svg.prop("width", "500"), svg.prop("height", "500")),
children: string.concat(
svg.rect({
props: string.concat(svg.prop("width", "500"), svg.prop("height", "500"), svg.prop("fill", "lightgray"))
}),
svg.text({
props: string.concat(
svg.prop("x", "50%"),
svg.prop("y", "50%"),
svg.prop("dominant-baseline", "middle"),
svg.prop("text-anchor", "middle"),
svg.prop("font-size", "48"),
svg.prop("fill", "black")
),
children: string.concat("You're looking at slip #", tokenId.toString())
})
)
});
}

function mint(address to) public {
unchecked {
_mint(to, ++currentId);
}
}

function isOwnerOrApproved(uint256 tokenId, address addr) internal view virtual override returns (bool) {
return ownerOf(tokenId) == addr || getApproved(tokenId) == addr || isApprovedForAll(ownerOf(tokenId), addr);
}

function supportsInterface(bytes4 interfaceId) public view virtual override(DynamicTraits, ERC721) returns (bool) {
return DynamicTraits.supportsInterface(interfaceId) || ERC721.supportsInterface(interfaceId);
}

function myVeryOwnFunction() public {
emit SayHiToMom("Hey Mama!");
}
}
```

(You'll notice that this looks a lot like shipyard-core's ExampleNFT.sol, but it's special to me now!)

What's great about this is that the `ERC721ConduitPreapproved_Solady` I inheritied via `AbstractNFT` has already handled all the routine ERC721 functionality for me in an almost comically optimized way. So I can burn calories on making something cool, instead of reinventing the nonfungible token wheel. But when I call the `ownerOf` function on my contract, it's going to work as expected.

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.

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.

## Next up:

[Deploying for real](Deploying.md)
Loading