diff --git a/.env.example b/.env.example index 14d938c8..70020f63 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,10 @@ -MAINNET_RPC= -MAINNET_DEPLOYER_NAME= +GNOSIS_RPC='https://rpc.gnosis.gateway.fm' +GNOSIS_DEPLOYER_PK= + +OPTIMISM_RPC= +OPTIMISM_DEPLOYER_PK= SEPOLIA_RPC= -SEPOLIA_DEPLOYER_NAME= +SEPOLIA_DEPLOYER_PK= ETHERSCAN_API_KEY= diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 18624462..0bb9ae11 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ concurrency: cancel-in-progress: true env: - MAINNET_RPC: ${{ secrets.MAINNET_RPC }} + OPTIMISM_RPC: ${{ secrets.OPTIMISM_RPC }} SEPOLIA_RPC: ${{ secrets.SEPOLIA_RPC }} jobs: diff --git a/README.md b/README.md index 5a4d6546..158f7cc9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,48 @@ <br /> +<div align="center">Forked by Breadchain</div> + +## Breadchain Developers + +<dl> + <dt>Basic Setup</dt> + <dd>Use `yarn add` / `yarn build`, not `forge install` / `forge build` etc.</dd> + <dd>Use yarn scripts, not forge scripts (Look at the `package.json`!).</dd> + <dd>Add scripts to the `package.json` as needed.</dd> + + <dt>Required Contracts</dt> + <dd>Every contract is required to have a full interface.</dd> + <dd>The contract should inherit it's own interface.</dd> + <dd>The contract should reference it's interface with `@inheritdoc`</dd> + <dd>The interface should contain other tags, including `@notice, @param, @return, @dev`</dd> + <dd>The contract should be free of clutter, whereas the interface should act as a guide to the contract.</dd> + <dd>Errors, Events, and Structs should be located in interfaces, not in contracts.</dd> + + <dt>Required Testing</dt> + <dd><b>Integration/E2E</b> - should fork chain intended for deployment (likely gnosis or optimism) and should use the deploy script found in `Script/Common.sol` in the `test/integration/IntegrationBase.sol`.</dd> + <dd><b>Unit</b> - should test and branch all contract functionality, with mock contracts or mock function calls added as needed to mock inter-contract calls. `.tree` files are there for example, but not necessary.</dd> + + <dt>Test Coverage</dt> + <dd>Run `yarn coverage` to generate a coverage report for tests</dd> + <dd>Unit and Integration should be 100%, with branch testing reasonably high</dd> + + <dt>Advice for Writing Tests</dt> + <dd>Make use of `setUp` overrides and inheritance to cut down on redundant setups.</dd> + <dd>Make use of helper functions with obvious names (e.g. `_fundUsersWithTokens()`) to reduce complexity for other developers that will review the code.</dd> + <dd>Make use of Modifiers within test contracts to constrain fuzzing variables or any other use case</dd> + <dd>Make multiple testing contracts in one file that all test the same contract (e.g. E2EGreeterTestSetup, E2EGreeterTestAccessControl, E2EGreeterTestCore, etc.) where contracts inherit the same base setup.</dd> + <dd>Make use of constant variables placed in base test setup, so that updating test variables is simple and easy.</dd> + <dd>Keep tests organized! Break complexity down and make it readable!</dd> + + <dt>Required Formatting</dt> + <dd>Commits cannot be made without passing the linter!</dd> + <dd>Run `yarn lint:check`</dd> + <dd>Check `package.json` for more linter commands</dd> + <dd>Internal variables start with an underscore ('_exampleOfInternalVar').</dd> + <dd>Run `lint:natspec` to check contracts and interfaces for correct natspec.</dd> +</dl> + ## Features <dl> @@ -113,7 +155,7 @@ source .env Import your private keys into Foundry's encrypted keystore: ```bash -cast wallet import $MAINNET_DEPLOYER_NAME --interactive +cast wallet import $OPTIMISM_DEPLOYER_NAME --interactive ``` ```bash diff --git a/foundry.toml b/foundry.toml index fcadc272..ce135c6a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -25,12 +25,14 @@ out = 'out-via-ir' src = 'src/interfaces/' [fuzz] -runs = 1000 +runs = 256 [rpc_endpoints] -mainnet = "${MAINNET_RPC}" -sepolia = "${SEPOLIA_RPC}" +gnosis = 'https://rpc.gnosis.gateway.fm' +optimism = 'https://mainnet.optimism.io' +sepolia = 'https://rpc.sepolia.io' [etherscan] -mainnet = { key = "${ETHERSCAN_API_KEY}" } +gnosis = { key = "${ETHERSCAN_API_KEY}" } +optimism = { key = "${ETHERSCAN_API_KEY}" } sepolia = { key = "${ETHERSCAN_API_KEY}" } diff --git a/package.json b/package.json index 88242ec3..8ac1828e 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,9 @@ "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "coverage": "forge coverage --report summary --report lcov --match-path 'test/unit/*'", - "deploy:mainnet": "bash -c 'source .env && forge script Deploy --rpc-url $MAINNET_RPC --account $MAINNET_DEPLOYER_NAME --broadcast --verify --chain mainnet -vvvvv'", - "deploy:sepolia": "bash -c 'source .env && forge script Deploy --rpc-url $SEPOLIA_RPC --account $SEPOLIA_DEPLOYER_NAME --broadcast --verify --chain sepolia -vvvvv'", + "deploy:gnosis": "bash -c 'source .env && forge script Deploy --rpc-url $GNOSIS_RPC --private-key $GNOSIS_PK --broadcast --verify --chain gnosis -vvvvv'", + "deploy:optimism": "bash -c 'source .env && forge script Deploy --rpc-url $OPTIMISM_RPC --private-key $OPTIMISM_PK --broadcast --verify --chain optimism -vvvvv'", + "deploy:sepolia": "bash -c 'source .env && forge script Deploy --rpc-url $SEPOLIA_RPC --private-key $SEPOLIA_PK --broadcast --verify --chain sepolia -vvvvv'", "lint:check": "yarn lint:sol && forge fmt --check", "lint:fix": "sort-package-json && forge fmt && yarn lint:sol --fix", "lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js", @@ -32,6 +33,10 @@ "(src|test|script)/**/*.sol": "yarn lint:sol", "package.json": "sort-package-json" }, + "dependencies": { + "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts-upgradeable": "^5.1.0" + }, "devDependencies": { "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "19.2.2", diff --git a/remappings.txt b/remappings.txt index 0ba2eecd..80f099bd 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,5 +1,8 @@ forge-std/=node_modules/forge-std/src halmos-cheatcodes=node_modules/halmos-cheatcodes +@openzeppelin/=node_modules/@openzeppelin/contracts/ +@openzeppelin-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ + contracts/=src/contracts interfaces/=src/interfaces diff --git a/script/Common.sol b/script/Common.sol new file mode 100644 index 00000000..a5788934 --- /dev/null +++ b/script/Common.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol'; +import {Greeter, IGreeter} from 'contracts/Greeter.sol'; +import {Script} from 'forge-std/Script.sol'; +// solhint-disable-next-line +import 'script/Registry.sol'; + +/** + * @title Common Contract + * @author Breadchain + * @notice This contract is used to deploy the Greeter contract + * @dev This contract is intended for use in Scripts and Integration Tests + */ +contract Common is Script { + struct DeploymentParams { + string greeting; + IERC20 token; + } + + IGreeter public greeter; + + /// @notice Deployment parameters for each chain + mapping(uint256 _chainId => DeploymentParams _params) internal _deploymentParams; + + function setUp() public virtual { + // Optimism + _deploymentParams[10] = DeploymentParams('Hello, Optimism!', IERC20(OPTIMISM_DAI)); + + // Gnosis + _deploymentParams[100] = DeploymentParams('Hello, Gnosis!', IERC20(GNOSIS_BREAD)); + } + + function _deployContracts() internal { + DeploymentParams memory _params = _deploymentParams[block.chainid]; + + greeter = new Greeter(_params.greeting, _params.token); + } +} diff --git a/script/Deploy.sol b/script/Deploy.sol index 7ce78e9c..9bfcd0c7 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -1,33 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Greeter} from 'contracts/Greeter.sol'; -import {Script} from 'forge-std/Script.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; - -contract Deploy is Script { - struct DeploymentParams { - string greeting; - IERC20 token; - } - - /// @notice Deployment parameters for each chain - mapping(uint256 _chainId => DeploymentParams _params) internal _deploymentParams; - - function setUp() public { - // Mainnet - _deploymentParams[1] = DeploymentParams('Hello, Mainnet!', IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2)); - - // Sepolia - _deploymentParams[11_155_111] = - DeploymentParams('Hello, Sepolia!', IERC20(0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6)); - } +import {Common} from 'script/Common.sol'; +contract Deploy is Common { function run() public { - DeploymentParams memory _params = _deploymentParams[block.chainid]; - vm.startBroadcast(); - new Greeter(_params.greeting, _params.token); + + _deployContracts(); + vm.stopBroadcast(); } } diff --git a/script/Registry.sol b/script/Registry.sol new file mode 100644 index 00000000..613a708e --- /dev/null +++ b/script/Registry.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +/// @dev Example of addresses that may be stored in the Registry for use throughout the repository + +// Tokens (Optimism Chain) +address constant OPTIMISM_DAI = 0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1; + +// Tokens (Gnosis Chain) +address constant GNOSIS_BREAD = 0xa555d5344f6FB6c65da19e403Cb4c1eC4a1a5Ee3; +address constant GNOSIS_XDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d; + +// Curve Factory (Gnosis Chain) +address constant GNOSIS_CURVE_STABLE_SWAP_FACTORY = 0xbC0797015fcFc47d9C1856639CaE50D0e69FbEE8; + +// Liquidity Pools (Gnosis Chain) +address constant GNOSIS_CURVE_POOL_XDAI_BREAD = 0xf3D8F3dE71657D342db60dd714c8a2aE37Eac6B4; diff --git a/src/contracts/Greeter.sol b/src/contracts/Greeter.sol index 7167b5c7..54e3c660 100644 --- a/src/contracts/Greeter.sol +++ b/src/contracts/Greeter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol'; import {IGreeter} from 'interfaces/IGreeter.sol'; contract Greeter is IGreeter { @@ -44,7 +44,7 @@ contract Greeter is IGreeter { /// @inheritdoc IGreeter function greet() external view returns (string memory _greeting, uint256 _balance) { _greeting = greeting; - _balance = token.balanceOf(msg.sender); + _balance = token.balanceOf(address(this)); } /// @inheritdoc IGreeter diff --git a/src/interfaces/IGreeter.sol b/src/interfaces/IGreeter.sol index 53b4a64a..00b11806 100644 --- a/src/interfaces/IGreeter.sol +++ b/src/interfaces/IGreeter.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol'; /** * @title Greeter Contract - * @author Wonderland + * @author Breadchain * @notice This is a basic contract created in order to portray some * best practices and foundry functionality. */ diff --git a/test/integration/Greeter.t.sol b/test/integration/Greeter.t.sol index b0e3799d..ef65ff36 100644 --- a/test/integration/Greeter.t.sol +++ b/test/integration/Greeter.t.sol @@ -4,13 +4,25 @@ pragma solidity 0.8.23; import {IntegrationBase} from 'test/integration/IntegrationBase.sol'; contract IntegrationGreeter is IntegrationBase { + string internal _newGreeting; + + function setUp() public override { + /// @dev override for more specific setup + super.setUp(); + _newGreeting = 'Hello, Breadchain!'; + } + function test_Greet() public { - uint256 _whaleBalance = _dai.balanceOf(_daiWhale); + DeploymentParams memory _params = _deploymentParams[block.chainid]; + + (string memory _initialGreeting, uint256 _balance) = greeter.greet(); + assertEq(_params.greeting, _initialGreeting); + assertEq(INIT_BALANCE, _balance); - vm.prank(_daiWhale); - (string memory _greeting, uint256 _balance) = _greeter.greet(); + vm.prank(owner); + greeter.setGreeting(_newGreeting); - assertEq(_whaleBalance, _balance); - assertEq(_initialGreeting, _greeting); + (string memory _currentGreeting,) = greeter.greet(); + assertEq(_currentGreeting, greeter.greeting()); } } diff --git a/test/integration/IntegrationBase.sol b/test/integration/IntegrationBase.sol index b043f497..7531ce79 100644 --- a/test/integration/IntegrationBase.sol +++ b/test/integration/IntegrationBase.sol @@ -1,23 +1,29 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; -import {Greeter, IGreeter} from 'contracts/Greeter.sol'; +import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol'; import {Test} from 'forge-std/Test.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {Common} from 'script/Common.sol'; +// solhint-disable-next-line +import 'script/Registry.sol'; -contract IntegrationBase is Test { - uint256 internal constant _FORK_BLOCK = 18_920_905; +contract IntegrationBase is Common, Test { + uint256 public constant INIT_BALANCE = 1 ether; - string internal _initialGreeting = 'hola'; - address internal _user = makeAddr('user'); - address internal _owner = makeAddr('owner'); - address internal _daiWhale = 0x42f8CA49E88A8fd8F0bfA2C739e648468b8f9dec; - IERC20 internal _dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - IGreeter internal _greeter; + address public user = makeAddr('user'); + address public owner = makeAddr('owner'); - function setUp() public { - vm.createSelectFork(vm.rpcUrl('mainnet'), _FORK_BLOCK); - vm.prank(_owner); - _greeter = new Greeter(_initialGreeting, _dai); + IERC20 public bread = IERC20(GNOSIS_BREAD); + + function setUp() public virtual override { + super.setUp(); + vm.createSelectFork(vm.rpcUrl('gnosis')); + vm.startPrank(owner); + + /// @dev deploy contracts methods are located in script/Common.sol + _deployContracts(); + + vm.stopPrank(); + deal(GNOSIS_BREAD, address(greeter), INIT_BALANCE); } } diff --git a/test/unit/Greeter.t.sol b/test/unit/Greeter.t.sol index 82f59c91..c9a7f484 100644 --- a/test/unit/Greeter.t.sol +++ b/test/unit/Greeter.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.23; +import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol'; import {Greeter, IGreeter} from 'contracts/Greeter.sol'; import {Test} from 'forge-std/Test.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; contract UnitGreeter is Test { address internal _owner = makeAddr('owner'); diff --git a/yarn.lock b/yarn.lock index 59ef3222..d301a5da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -223,6 +223,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@openzeppelin/contracts-upgradeable@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.1.0.tgz#4d37648b7402929c53e2ff6e45749ecff91eb2b6" + integrity sha512-AIElwP5Ck+cslNE+Hkemf5SxjJoF4wBvvjxc27Rp+9jaPs/CLIaUBMYe1FNzhdiN0cYuwGRmYaRHmmntuiju4Q== + +"@openzeppelin/contracts@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.1.0.tgz#4e61162f2a2bf414c4e10c45eca98ce5f1aadbd4" + integrity sha512-p1ULhl7BXzjjbha5aqst+QMLY+4/LCWADXOCsmLHRM77AqiPjnd9vvUN9sosUfhL9JGKpZ0TjEGxgvnizmWGSA== + "@scure/base@~1.1.4": version "1.1.6" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d"