diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d600a7e --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +## Add your Etherscan (or altrnative network variant) to verify your smart contract on Etherscam +ETHERSCAN_API_KEY= + +## Override the default --from address when deploying on BTP +# ETH_FROM=0x0000000000000000000000000000000000000000 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..553e227 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,15 @@ +version: v1 + +labels: + - label: 'feat' + matcher: + title: '^feat:.*' + - label: 'fix' + matcher: + title: '^fix:.*' + - label: 'chore' + matcher: + title: '^chore:.*' + - label: 'docs' + matcher: + title: '^docs:.*' diff --git a/.github/release.yaml b/.github/release.yaml new file mode 100644 index 0000000..a57fc03 --- /dev/null +++ b/.github/release.yaml @@ -0,0 +1,23 @@ +changelog: + exclude: + authors: + - renovate + categories: + - title: Breaking Changes 🛠 + labels: + - breaking-change + - title: Exciting New Features 🎉 + labels: + - feat + - title: Important Bug Fixes 🐛 + labels: + - fix + - title: Documentation 📚 + labels: + - docs + - title: Other changes 🏗️ + labels: + - chore + - title: Dependencies 📦 + labels: + - dependencies diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..1ae15b5 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":automergeMinor", + ":automergePr", + ":automergeRequireAllStatusChecks", + ":gitSignOff", + ":pinVersions", + ":semanticCommits", + ":semanticCommitTypeAll(chore)", + ":enableVulnerabilityAlerts", + ":combinePatchMinorReleases", + ":prConcurrentLimitNone", + ":prHourlyLimitNone", + "security:openssf-scorecard", + "schedule:nonOfficeHours" + ], + "labels": ["dependencies"], + "rebaseWhen": "conflicted", + "packageRules": [], + "hostRules": [ + { + "timeout": 3000000 + } + ] +} diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml new file mode 100644 index 0000000..f1855da --- /dev/null +++ b/.github/workflows/branch.yml @@ -0,0 +1,109 @@ +name: Branch + +on: + pull_request: + branches: + - main + push: + branches: + - main + tags: + - "v*" + +env: + FOUNDRY_PROFILE: ci + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + actions: write + checks: write + contents: write + deployments: write + id-token: write + issues: write + discussions: write + packages: write + pages: write + pull-requests: write + repository-projects: write + security-events: write + statuses: write + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: namespace-profile-foundry + steps: + - name: Checkout + uses: namespacelabs/nscloud-checkout-action@v2 + with: + submodules: recursive + + - name: Setup caches + uses: namespacelabs/nscloud-cache-action@v1 + with: + path: | + cache + out + ~/.foundry + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_PASS }} + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + # list of Docker images to use as base name for tags + images: | + ghcr.io/settlemint/solidity-empty + # generate Docker tags based on the following events/attributes + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + no-cache: true diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 0000000..20bf969 --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,28 @@ +name: PR Labels + +on: + pull_request: + types: [opened, closed] + branches: + - main + +permissions: + actions: write + checks: write + contents: write + deployments: write + id-token: write + issues: write + discussions: write + packages: write + pages: write + pull-requests: write + repository-projects: write + security-events: write + statuses: write + +jobs: + labels: + runs-on: ubuntu-latest + steps: + - uses: fuxingloh/multi-labeler@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8dca3a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +# Subgraphs +deployment.txt +deployment-anvil.txt +subgraph/subgraph.config.json +subgraph/node_modules +subgraph/generated +subgraph/build + +.pnpm \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..00574f3 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "nomicfoundation.hardhat-solidity", + "miguelsolorio.fluent-icons", + "vscode-icons-team.vscode-icons", + "genieai.chatgpt-vscode", + "esbenp.prettier-vscode", + "dracula-theme.theme-dracula", + "cnshenj.vscode-task-manager" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..74dfcb5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,114 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "make build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] + }, + { + "label": "test", + "type": "shell", + "command": "make test", + "group": "test", + "problemMatcher": [] + }, + { + "label": "format", + "type": "shell", + "command": "make format", + "problemMatcher": [] + }, + { + "label": "snapshot", + "type": "shell", + "command": "make snapshot", + "problemMatcher": [] + }, + { + "label": "anvil", + "type": "shell", + "command": "make anvil", + "problemMatcher": [] + }, + { + "label": "deploy-anvil", + "type": "shell", + "command": "make deploy-anvil", + "problemMatcher": [] + }, + { + "label": "deploy", + "type": "shell", + "command": "EXTRA_ARGS=\"${input:extra-deployment-verify} ${input:extra-deployment-other}\" make deploy", + "problemMatcher": [] + }, + { + "label": "script-anvil", + "type": "shell", + "command": "EXTRA_ARGS=\"${input:extra-script-broadcast} ${input:extra-script-other}\" make script-anvil", + "problemMatcher": [] + }, + { + "label": "script", + "type": "shell", + "command": "EXTRA_ARGS=\"${input:extra-script-broadcast} ${input:extra-script-other}\" make script", + "problemMatcher": [] + }, + { + "label": "cast", + "type": "shell", + "command": "make cast", + "problemMatcher": [] + }, + { + "label": "subgraph", + "type": "shell", + "command": "make subgraph", + "problemMatcher": [] + }, + { + "label": "help", + "type": "shell", + "command": "make help", + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "extra-deployment-verify", + "description": "Extra deployment options?", + "default": "", + "type": "pickString", + "options": [ + "", + "--verify --verifier sourcify", + "--verify --verifier etherscan --etherscan-api-key ${ETHERSCAN_API_KEY}" + ] + }, + { + "id": "extra-deployment-other", + "description": "Other extra deployment options?", + "default": "", + "type": "promptString" + }, + { + "id": "extra-script-broadcast", + "description": "Broadcast?", + "default": "", + "type": "pickString", + "options": ["", "--broadcast"] + }, + { + "id": "extra-script-other", + "description": "Other extra script options?", + "default": "", + "type": "promptString" + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..425aeb0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:21.6.2-bookworm as build + +ENV FOUNDRY_DIR /usr/local +RUN curl -L https://foundry.paradigm.xyz | bash && \ + /usr/local/bin/foundryup + +WORKDIR / + +RUN git config --global user.email "hello@settlemint.com" && \ + git config --global user.name "SettleMint" && \ + forge init usecase --template settlemint/solidity-empty && \ + cd usecase && \ + forge build + +USER root + +FROM busybox + +COPY --from=build /usecase /usecase +COPY --from=build /root/.svm /usecase-svm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5ab2be5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 SettleMint + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2002fd9 --- /dev/null +++ b/Makefile @@ -0,0 +1,92 @@ +# Makefile for Foundry Ethereum Development Toolkit + +.PHONY: build test format snapshot anvil deploy deploy-anvil cast help subgraph + +build: + @echo "Building with Forge..." + @forge build + +test: + @echo "Testing with Forge..." + @forge test + +format: + @echo "Formatting with Forge..." + @forge fmt + +snapshot: + @echo "Creating gas snapshot with Forge..." + @forge snapshot + +anvil: + @echo "Starting Anvil local Ethereum node..." + @anvil + +deploy-anvil: + @echo "Deploying with Forge to Anvil..." + @forge create ./src/Counter.sol:Counter --rpc-url anvil --interactive | tee deployment-anvil.txt + +deploy: + @eval $$(curl -H "x-auth-token: $${BPT_SERVICE_TOKEN}" -s $${BTP_CLUSTER_MANAGER_URL}/ide/foundry/$${BTP_SCS_ID}/env | sed 's/^/export /') + @if [ -z "${ETH_FROM}" ]; then \ + echo "\033[1;33mWARNING: No keys are activated on the node, falling back to interactive mode...\033[0m"; \ + echo ""; \ + forge create ./src/Counter.sol:Counter ${EXTRA_ARGS} --rpc-url ${BTP_RPC_URL} --interactive | tee deployment.txt; \ + else \ + forge create ./src/Counter.sol:Counter ${EXTRA_ARGS} --rpc-url ${BTP_RPC_URL} --unlocked | tee deployment.txt; \ + fi + +script-anvil: + @if [ ! -f deployment-anvil.txt ]; then \ + echo "\033[1;31mERROR: Contract was not deployed or the deployment-anvil.txt went missing.\033[0m"; \ + exit 1; \ + fi + @DEPLOYED_ADDRESS=$$(grep "Deployed to:" deployment-anvil.txt | awk '{print $$3}') forge script script/Counter.s.sol:CounterScript ${EXTRA_ARGS} --rpc-url anvil -i=1 + +script: + @if [ ! -f deployment-anvil.txt ]; then \ + echo "\033[1;31mERROR: Contract was not deployed or the deployment-anvil.txt went missing.\033[0m"; \ + exit 1; \ + fi + @eval $$(curl -H "x-auth-token: $${BPT_SERVICE_TOKEN}" -s $${BTP_CLUSTER_MANAGER_URL}/ide/foundry/$${BTP_SCS_ID}/env | sed 's/^/export /') + @if [ -z "${ETH_FROM}" ]; then \ + echo "\033[1;33mWARNING: No keys are activated on the node, falling back to interactive mode...\033[0m"; \ + echo ""; \ + @DEPLOYED_ADDRESS=$$(grep "Deployed to:" deployment.txt | awk '{print $$3}') forge script script/Counter.s.sol:CounterScript ${EXTRA_ARGS} --rpc-url ${BTP_RPC_URL} -i=1; \ + else \ + @DEPLOYED_ADDRESS=$$(grep "Deployed to:" deployment.txt | awk '{print $$3}') forge script script/Counter.s.sol:CounterScript ${EXTRA_ARGS} --rpc-url ${BTP_RPC_URL} --unlocked; \ + fi + +cast: + @echo "Interacting with EVM via Cast..." + @cast $(SUBCOMMAND) + +subgraph: + @echo "Deploying the subgraph..." + @rm -Rf subgraph/subgraph.config.json + @DEPLOYED_ADDRESS=$$(grep "Deployed to:" deployment.txt | awk '{print $$3}') yq e -p=json -o=json '.datasources[0].address = env(DEPLOYED_ADDRESS) | .chain = env(BTP_NETWORK_NAME)' subgraph/subgraph.config.template.json > subgraph/subgraph.config.json + @cd subgraph + @pnpm graph-compiler --config subgraph.config.json --include node_modules/@openzeppelin/subgraphs/src/datasources subgraph/datasources --export-schema --export-subgraph + @yq e '.specVersion = "0.0.4"' -i generated/solidity-token-erc20.subgraph.yaml + @yq e '.description = "Solidity Token ERC20"' -i generated/solidity-token-erc20.subgraph.yaml + @yq e '.repository = "https://github.com/settlemint/solidity-token-erc20"' -i generated/solidity-token-erc20.subgraph.yaml + @yq e '.indexerHints.prune = "auto"' -i generated/solidity-token-erc20.subgraph.yaml + @yq e '.features = ["nonFatalErrors", "fullTextSearch", "ipfsOnEthereumContracts"]' -i generated/solidity-token-erc20.subgraph.yaml + @pnpm graph codegen generated/solidity-token-erc20.subgraph.yaml + @pnpm graph build generated/solidity-token-erc20.subgraph.yaml + @eval $$(curl -H "x-auth-token: $${BPT_SERVICE_TOKEN}" -s $${BTP_CLUSTER_MANAGER_URL}/ide/foundry/$${BTP_SCS_ID}/env | sed 's/^/export /') + @if [ "$${BTP_MIDDLEWARE}" == "" ]; then \ + echo "You have not launched a graph middleware for this smart contract set, aborting..."; \ + exit 1; \ + else \ + pnpm graph create --node $${BTP_MIDDLEWARE} $${BTP_SCS_NAME}; \ + pnpm graph deploy --version-label v1.0.$$(date +%s) --node $${BTP_MIDDLEWARE} --ipfs $${BTP_IPFS}/api/v0 $${BTP_SCS_NAME} generated/solidity-token-erc20.subgraph.yaml; \ + fi + +help: + @echo "Forge help..." + @forge --help + @echo "Anvil help..." + @anvil --help + @echo "Cast help..." + @cast --help diff --git a/README.md b/README.md new file mode 100644 index 0000000..85f539e --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# ERC20 + +A basic ERC20 token contract. + +## Get started + +Launch this smart contract set in the SettleMint Blockchain Transformation platform under the `Smart Contract Sets` section. This will automatically link it to your own blockchain node and make use of the private keys living in the platform. + +If you want to use it separately, bootstrap a new project using + +```shell +forge init my-erc20-token --template settlemint/solidity-token-erc20 +``` + +## DX: Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +- https://console.settlemint.com/documentation/docs/using-platform/integrated-development-environment/ +- https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +Anvil is a local development node, open a terminal in the IDE and launch anvil. You can then deploy to it using `make deploy-anvil` + +```shell +$ anvil +``` + +### Deploy + +Deploy to a local anvil node: + +```shell +$ make deploy-anvil +``` + +Deploy to the connected platform node: + +```shell +$ make deploy-btp +``` + +### Cast + +```shell +$ cast +``` + +### Deploy your subgraph + +```shell +$ make subgraph +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..b1e34f8 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,31 @@ +[profile.ci] + fuzz = { runs = 10_000 } + verbosity = 4 + +[fmt] + bracket_spacing = true + int_types = "long" + line_length = 120 + multiline_func_header = "all" + number_underscore = "thousands" + quote_style = "double" + tab_width = 4 + wrap_comments = true + +[rpc_endpoints] +anvil = "http://localhost:8545" +btp = "${BTP_RPC_URL}" + +[profile.default] + src = "src" + test = "test" + out = "out" + libs = ["lib"] + solc = "0.8.19" + optimizer = true + optimizer_runs = 10_000 + gas_reports = ["*"] + fuzz = { runs = 1_000 } + auto_detect_solc = false + extra_output_files = [ "metadata" ] + fs_permissions = [{ access = "read", path = "./deployment-anvil.txt"}, { access = "read", path = "./deployment.txt"} ] \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..ae570fe --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce diff --git a/script/Counter.s.sol b/script/Counter.s.sol new file mode 100644 index 0000000..af43591 --- /dev/null +++ b/script/Counter.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Script, console} from "forge-std/Script.sol"; +import "../src/Counter.sol"; + +contract CounterScript is Script { + Counter public counter; // Instance of the Counter contract + + function setUp() public { + string memory key = "DEPLOYED_ADDRESS"; + counter = Counter(vm.envAddress(key)); + } + + function run() public { + vm.startBroadcast(); + + counter.setNumber(5); // Set the initial number + + uint256 initialCount = counter.number(); // Read the current value of the counter + console.log("Current Counter Value:", initialCount); // Log the current counter value + + counter.increment(); // Call the increment function of the Counter contract + + uint256 currentCount = counter.number(); // Read the current value of the counter + console.log("Current Counter Value:", currentCount); // Log the current counter value + } +} \ No newline at end of file diff --git a/src/Counter.sol b/src/Counter.sol new file mode 100644 index 0000000..f0e44a0 --- /dev/null +++ b/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol new file mode 100644 index 0000000..b55ec1f --- /dev/null +++ b/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +}