Skip to content

Commit

Permalink
Merge pull request #712 from OpenZeppelin/foundry
Browse files Browse the repository at this point in the history
✨ Port all contracts to Foundry
  • Loading branch information
xaler5 authored Apr 5, 2024
2 parents c8860dc + 3432dca commit ebf7e29
Show file tree
Hide file tree
Showing 492 changed files with 9,969 additions and 11,644 deletions.
33 changes: 25 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
name: Test

on:
push:
branches: [master]
pull_request: {}

jobs:
test:
tests:
name: Foundry tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./contracts

steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16.x
- run: yarn
- run: yarn test:contracts
- uses: actions/checkout@v3
with:
submodules: true

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Install dependencies
run: forge install

- name: Check contract sizes
run: forge build --sizes --skip test
id: build

- name: Run tests
run: forge test -v
id: test
9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ node_modules/
client/.env
contracts/.env

# Contracts
contracts/build/
contracts/cache/
contracts/node_modules/
contracts/.openzeppelin/

# Client
client/build/
client/node_modules/
client/src/gamedata/deploy.local.json
.env

# Local Netlify folder
.netlify
12 changes: 12 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/lib/openzeppelin-contracts-06"]
path = contracts/lib/openzeppelin-contracts-06
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "contracts/lib/openzeppelin-contracts-08"]
path = contracts/lib/openzeppelin-contracts-08
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "contracts/lib/openzeppelin-contracts-upgradeable"]
path = contracts/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## March 2024
- Updated all contracts (challenges, tests, etc.) to use Foundry instead of Hardhat

## August 2021
- Filter out repeated log messages for mined transactions
- Increased Sidebar width
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ If you would like to contribute in another way, please reach out to us via email

*A level is composed of the following elements:*

- A `<Level>Factory.sol` contract, where `<Level>` is replaced by the name of the level, that needs to extend [`Level.sol`](./contracts/contracts/levels/base/Level.sol). This factory contract will be deployed only once and registered on Ethernaut.sol by Ethernaut's owner. Players never interact with the factory directly. The factory is in charge of creating level instances for players to use (1 instance per player) and to check these instances to verify if the player has passed the level. Factories should not have state that can be changed by the player.
- A `<Level>Factory.sol` contract, where `<Level>` is replaced by the name of the level, that needs to extend [`Level.sol`](./contracts/src/levels/base/Level.sol). This factory contract will be deployed only once and registered on Ethernaut.sol by Ethernaut's owner. Players never interact with the factory directly. The factory is in charge of creating level instances for players to use (1 instance per player) and to check these instances to verify if the player has passed the level. Factories should not have state that can be changed by the player.
- A `level instance` contract named `<Level>.sol`, where `<Level>` is replaced by the name of the level, that is emitted by the factory for each player that requests it. Instances need to be completely decouppled from Ethernaut's architecture. Factories will emit them and verify them. That is, level instances don't know anything about their factories or Ethernaut. An instance's state can be completely demolished by players and even destroyed since they are not really part of the architecture, just a challenge for a player to use at will.
- A `description file` in [the descriptions directory](./client/src/gamedata/en/descriptions/levels) that the UI presents to the player and describes the level's objectives with some narrative and tips.
- A `description completion file` also located in [the descriptions directory](./client/src/gamedata/en/descriptions/levels) that the UI presents to the player when the level is passed, further information about the player, historical insights, further explanations or just a congrats message.
Expand All @@ -60,8 +60,8 @@ Let's suppose that we are creating the level "King" (which is already created an
2. Use the other levels as a basis, eg. duplicate DummyFactory.sol and Dummy.sol.
3. Rename and modify the contracts to KingFactory.sol and King.sol respectively.
4. Implement the desired instance and factory logic in solidity. See current levels and notes to understand how the game mechanics work.
5. Add the test file `contracts/test/levels/King.test.js`. Use other tests files as reference to see how tests might work.
6. Run `yarn test:contracts` and once all tests pass, register the level in [gamedata.json](client/src/gamedata/gamedata.json).
5. Add the test file `contracts/test/levels/King.t.sol`. Use other tests files as reference to see how tests might work.
6. Run `forge test` and once all tests pass, register the level in [gamedata.json](client/src/gamedata/gamedata.json).
7. The level should now show up in the ui. To start the UI, set the [ACTIVE_NETWORK](client/src/constants.js) to `NETWORKS.LOCAL` and run `yarn start`.
8. Add a description markdown file, in this case client/src/gamedata/levels/king.md (make sure gamedata.json points to it). This content will now be displayed in the ui for the level.
9. Add a completed description markdown file, in this case client/src/gamedata/levels/king_complete.md (make sure gamedata.json points to it). The level will display this as additional info once the level is solved, usually to include historical information related to the level.
Expand Down
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
"private": true,
"dependencies": {
"@sentry/react": "^7.0.0",
"@sentry/tracing": "^7.108.0",
"@truffle/contract": "^4.3.15",
"alchemy-sdk": "^2.2.3",
"axios": "^1.2.4",
"bad-words": "^3.0.4",
"bootstrap": "^5.0.0",
"contracts": "0.1.0",
"cross-env": "7.0.3",
"devtools-detect": "^4.0.0",
"dotenv": "^16.0.3",
Expand Down
1 change: 0 additions & 1 deletion client/public/contracts

This file was deleted.

10 changes: 5 additions & 5 deletions client/scripts/deploy_contracts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import * as ethutil from "../src/utils/ethutil.js";
import * as constants from "../src/constants.js";
import HDWalletProvider from "@truffle/hdwallet-provider";
import * as gamedata from "../src/gamedata/gamedata.json" assert { type: "json" };
import * as EthernautABI from "contracts/build/contracts/Ethernaut.sol/Ethernaut.json" assert { type: "json" };
import * as ProxyAdminABI from "contracts/build/contracts/proxy/ProxyAdmin.sol/ProxyAdmin.json" assert { type: "json" };
import * as ImplementationABI from "contracts/build/contracts/metrics/Statistics.sol/Statistics.json" assert { type: "json" };
import * as ProxyStatsABI from "contracts/build/contracts/proxy/ProxyStats.sol/ProxyStats.json" assert { type: "json" };
import * as EthernautABI from "../src/contracts/out/Ethernaut.sol/Ethernaut.json" assert { type: "json" };
import * as ProxyAdminABI from "../src/contracts/out/ProxyAdmin.sol/ProxyAdmin.json" assert { type: "json" };
import * as ImplementationABI from "../src/contracts/out/Statistics.sol/Statistics.json" assert { type: "json" };
import * as ProxyStatsABI from "../src/contracts/out/ProxyStats.sol/ProxyStats.json" assert { type: "json" };

let web3;
let ethernaut;
Expand Down Expand Up @@ -127,7 +127,7 @@ async function deployContracts(deployData) {
// Deploy contract
const LevelABI = JSON.parse(
fs.readFileSync(
`contracts/build/contracts/levels/${
`contracts/out/${
level.levelContract
}/${withoutExtension(level.levelContract)}.json`,
"utf-8"
Expand Down
14 changes: 7 additions & 7 deletions client/scripts/supersede_level.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import HDWalletProvider from "@truffle/hdwallet-provider";
import Web3 from "web3";
import * as ethutil from "../src/utils/ethutil.js";
import * as constants from "../src/constants.js";
import * as EthernautABI from "contracts/build/contracts/Ethernaut.sol/Ethernaut.json" assert { type: "json" };
import * as ProxyStatsABI from "contracts/build/contracts/proxy/ProxyStats.sol/ProxyStats.json" assert { type: "json" };
import * as ProxyAdminABI from "contracts/build/contracts/proxy/ProxyAdmin.sol/ProxyAdmin.json" assert { type: "json" };
import * as ImplementationABI from "contracts/build/contracts/metrics/Statistics.sol/Statistics.json" assert { type: "json" };
import * as SupersederImplementationABI from "contracts/build/contracts/metrics/StatisticsLevelSuperseder.sol/StatisticsLevelSuperseder.json" assert { type: "json" };
import * as EthernautABI from "../src/contracts/out/Ethernaut.sol/Ethernaut.json" assert { type: "json" };
import * as ProxyStatsABI from "../src/contracts/out/ProxyStats.sol/ProxyStats.json" assert { type: "json" };
import * as ProxyAdminABI from "../src/contracts/out/ProxyAdmin.sol/ProxyAdmin.json" assert { type: "json" };
import * as ImplementationABI from "../src/contracts/out/Statistics.sol/Statistics.json" assert { type: "json" };
import * as SupersederImplementationABI from "../src/contracts/out/StatisticsLevelSuperseder.sol/StatisticsLevelSuperseder.json" assert { type: "json" };

import gamedata from "../src/gamedata/gamedata.json" assert { type: "json" };
const levels = gamedata.levels;
Expand Down Expand Up @@ -201,7 +201,7 @@ async function deployAndUpgradeStatisticsToStatisticsSuperseder() {

const props = {
gasPrice: parseInt(await web3.eth.getGasPrice() * 1.10),
gas: 45000000,
gas: 30000000,
};
let from = constants.ADDRESSES[constants.ACTIVE_NETWORK.name];
if (!from) from = (await web3.eth.getAccounts())[0];
Expand Down Expand Up @@ -258,7 +258,7 @@ async function deployLevel(level) {

const LevelABI = JSON.parse(
fs.readFileSync(
`contracts/build/contracts/levels/${level.levelContract}/${
`contracts/out/${level.levelContract}/${
level.levelContract.split(".")[0]
}.json`,
"utf-8"
Expand Down
4 changes: 2 additions & 2 deletions client/scripts/upgrade_proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import fs from 'fs';
import * as ethutil from '../src/utils/ethutil.js';
import * as constants from '../src/constants.js';
import HDWalletProvider from '@truffle/hdwallet-provider';
import * as ProxyAdminABI from 'contracts/build/contracts/proxy/ProxyAdmin.sol/ProxyAdmin.json' assert { type: 'json' };
import * as ImplementationABI from 'contracts/build/contracts/metrics/Statistics.sol/Statistics.json' assert { type: 'json' };
import * as ProxyAdminABI from '../src/contracts/out/ProxyAdmin.sol/ProxyAdmin.json' assert { type: 'json' };
import * as ImplementationABI from '../src/contracts/out/Statistics.sol/Statistics.json' assert { type: 'json' };

let web3;

Expand Down
2 changes: 1 addition & 1 deletion client/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export const GOOGLE_ANALYTICS_ID = "UA-85043059-4";

// Owner addresses
export const ADDRESSES = {
[NETWORKS.LOCAL.name]: undefined,
[NETWORKS.LOCAL.name]: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
[NETWORKS.MUMBAI.name]: "0x09902A56d04a9446601a0d451E07459dC5aF0820",
[NETWORKS.SEPOLIA.name]: "0x09902A56d04a9446601a0d451E07459dC5aF0820",
[NETWORKS.OPTIMISM_SEPOLIA.name]: "0x09902A56d04a9446601a0d451E07459dC5aF0820",
Expand Down
2 changes: 1 addition & 1 deletion client/src/containers/Level.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class Level extends React.Component {

let sourcesFile = null;
try {
sourcesFile = require(`contracts/contracts/levels/${level.instanceContract}`);
sourcesFile = require(`../contracts/src/levels/${level.instanceContract}`);
} catch (e) {
console.log(e);
}
Expand Down
1 change: 1 addition & 0 deletions client/src/contracts
2 changes: 1 addition & 1 deletion client/src/middlewares/loadEthernautContract.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as ethutil from '../utils/ethutil'
import EthernautABI from 'contracts/build/contracts/Ethernaut.sol/Ethernaut.json'
import EthernautABI from '../contracts/out/Ethernaut.sol/Ethernaut.json'
import * as actions from '../actions';
import { loadTranslations } from '../utils/translations'

Expand Down
4 changes: 2 additions & 2 deletions client/src/middlewares/loadLevelInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ const loadLevelInstance = (store) => (next) => (action) => {
if (!instanceAddress) return;
console.info(`=> ${strings.instanceAddressMessage}\n${instanceAddress}`);
const Instance = ethutil.getTruffleContract(
require(`contracts/build/contracts/levels/${action.level.instanceContract
}/${withoutExtension(action.level.instanceContract)}.json`),
require(`../contracts/out/${action.level.instanceContract
}/${withoutExtension(action.level.instanceContract)}.json`),
{
from: state.player.address,
gasPrice: 2 * state.network.gasPrice,
Expand Down
6 changes: 3 additions & 3 deletions client/src/utils/contractutil.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as constants from "../constants";
import { getWeb3, setWeb3, getTruffleContract, getNetworkFromId } from "./ethutil";
import { newGithubIssueUrl } from "./github";
import * as LocalFactoryABI from "contracts/build/contracts/factory/LocalFactory.sol/Factory.json";
import * as LocalFactoryABI from "../contracts/out/LocalFactory.sol/Factory.json";
import { deployAdminContracts, deployAndRegisterLevel } from "./deploycontract";
var levels = require(`../gamedata/gamedata.json`).levels;

Expand Down Expand Up @@ -62,7 +62,7 @@ export function getLevelKey(levelAddress) {

export function fetchLevelABI(level) {
const contractName = level.levelContract.split(".")[0];
return require(`contracts/build/contracts/levels/${level.levelContract}/${contractName}.json`);
return require(`../contracts/out/${level.levelContract}/${contractName}.json`);
}

// write windows finction to transfer ownership to a new user
Expand Down Expand Up @@ -175,7 +175,7 @@ export const verifyContract = async (contractAddress, level, chainId) => {
if (!network.explorer || !level.verificationDetails)
return;

const contractFile = await fetch(`contracts/levels/${level.instanceContract}`);
const contractFile = await fetch(`../contracts/src/levels/${level.instanceContract}`);
const contractCode = await contractFile.text();

const headers = new Headers();
Expand Down
2 changes: 1 addition & 1 deletion client/src/utils/deploycontract.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import colors from "colors";
import * as ethutil from "../utils/ethutil.js";
import * as LocalFactoryABI from "contracts/build/contracts/factory/LocalFactory.sol/Factory.json";
import * as LocalFactoryABI from "../contracts/out/LocalFactory.sol/Factory.json";
import { getGasFeeDetails } from "../utils/ethutil.js";
import {
cacheContract,
Expand Down
2 changes: 1 addition & 1 deletion client/src/utils/ethutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const getTruffleContract = (jsonABI, defaults = {}) => {
// // With this, MetaMask v9 deprecation warnings are removed.
// const TruffleContract = require('@truffle/contract');

const truffleContract = TruffleContract(jsonABI);
const truffleContract = TruffleContract({ abi: jsonABI.abi, bytecode: jsonABI.bytecode.object });
if (!defaults.gasPrice) defaults.gasPrice = 2000000000;
if (!defaults.gas) defaults.gas = 2000000;
truffleContract.defaults(defaults);
Expand Down
2 changes: 1 addition & 1 deletion client/src/utils/statsContract.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const getProxyStatsContractAddressInNetwork = (networkId) => {
}

const getStatsContract = async (proxyStatsAddress, playerAddress) => {
const statsABI = require("contracts/build/contracts/metrics/Statistics.sol/Statistics.json");
const statsABI = require("../contracts/out/Statistics.sol/Statistics.json");
let statsContract;
if (playerAddress) {
statsContract = getTruffleContract(statsABI, { from: playerAddress });
Expand Down
13 changes: 13 additions & 0 deletions contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Compiler files
cache/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
66 changes: 66 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## 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://book.getfoundry.sh/

## Usage

### Build

```shell
$ forge build
```

### Test

```shell
$ forge test
```

### Format

```shell
$ forge fmt
```

### Gas Snapshots

```shell
$ forge snapshot
```

### Anvil

```shell
$ anvil
```

### Deploy

```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```

### Cast

```shell
$ cast <subcommand>
```

### Help

```shell
$ forge --help
$ anvil --help
$ cast --help
```
Loading

0 comments on commit ebf7e29

Please sign in to comment.