From f0dd8b5876e9440d5ab92072cc9d1930f5f7a398 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Sat, 12 Oct 2024 10:41:11 -0300 Subject: [PATCH] chore: project boilerplate --- .env.example | 8 -- .github/workflows/ci.yml | 29 ++++-- .gitpod.yml | 14 --- .prettierignore | 1 - .vscode/settings.json | 7 +- README.md | 217 ++++++++++----------------------------- bun.lockb | Bin 28441 -> 0 bytes foundry.toml | 81 +++++++-------- package.json | 12 ++- remappings.txt | 2 +- script/Base.s.sol | 2 +- script/Deploy.s.sol | 8 +- src/BlockfulToken.sol | 10 ++ src/Foo.sol | 8 -- src/TokenVendor.sol | 24 +++++ test/Foo.t.sol | 56 ---------- test/TokenVendor.t.sol | 120 ++++++++++++++++++++++ 17 files changed, 282 insertions(+), 317 deletions(-) delete mode 100644 .gitpod.yml delete mode 100755 bun.lockb create mode 100644 src/BlockfulToken.sol delete mode 100644 src/Foo.sol create mode 100644 src/TokenVendor.sol delete mode 100644 test/Foo.t.sol create mode 100644 test/TokenVendor.t.sol diff --git a/.env.example b/.env.example index 98c1028..51ab48b 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,3 @@ export API_KEY_ALCHEMY="YOUR_API_KEY_ALCHEMY" -export API_KEY_ARBISCAN="YOUR_API_KEY_ARBISCAN" -export API_KEY_BSCSCAN="YOUR_API_KEY_BSCSCAN" export API_KEY_ETHERSCAN="YOUR_API_KEY_ETHERSCAN" -export API_KEY_GNOSISSCAN="YOUR_API_KEY_GNOSISSCAN" -export API_KEY_INFURA="YOUR_API_KEY_INFURA" -export API_KEY_OPTIMISTIC_ETHERSCAN="YOUR_API_KEY_OPTIMISTIC_ETHERSCAN" -export API_KEY_POLYGONSCAN="YOUR_API_KEY_POLYGONSCAN" -export API_KEY_SNOWTRACE="YOUR_API_KEY_SNOWTRACE" -export MNEMONIC="YOUR_MNEMONIC" export FOUNDRY_PROFILE="default" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7550749..f3b9c5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,14 +21,17 @@ jobs: - name: "Install Foundry" uses: "foundry-rs/foundry-toolchain@v1" - - name: "Install Bun" - uses: "oven-sh/setup-bun@v1" + - name: Use Node.js 21 + uses: actions/setup-node@v3 + with: + node-version: 21 + cache: 'npm' - name: "Install the Node.js dependencies" - run: "bun install" + run: "npm install" - name: "Lint the code" - run: "bun run lint" + run: "npm run lint" - name: "Add lint summary" run: | @@ -44,11 +47,14 @@ jobs: - name: "Install Foundry" uses: "foundry-rs/foundry-toolchain@v1" - - name: "Install Bun" - uses: "oven-sh/setup-bun@v1" + - name: Use Node.js 21 + uses: actions/setup-node@v3 + with: + node-version: 21 + cache: 'npm' - name: "Install the Node.js dependencies" - run: "bun install" + run: "npm install" - name: "Build the contracts and print their size" run: "forge build --sizes" @@ -68,11 +74,14 @@ jobs: - name: "Install Foundry" uses: "foundry-rs/foundry-toolchain@v1" - - name: "Install Bun" - uses: "oven-sh/setup-bun@v1" + - name: Use Node.js 21 + uses: actions/setup-node@v3 + with: + node-version: 21 + cache: 'npm' - name: "Install the Node.js dependencies" - run: "bun install" + run: "npm install" - name: "Show the Foundry config" run: "forge config" diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index b9646d8..0000000 --- a/.gitpod.yml +++ /dev/null @@ -1,14 +0,0 @@ -image: "gitpod/workspace-bun" - -tasks: - - name: "Install dependencies" - before: | - curl -L https://foundry.paradigm.xyz | bash - source ~/.bashrc - foundryup - init: "bun install" - -vscode: - extensions: - - "esbenp.prettier-vscode" - - "NomicFoundation.hardhat-solidity" diff --git a/.prettierignore b/.prettierignore index 3996d20..4f2186e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,7 +10,6 @@ out *.log .DS_Store .pnp.* -bun.lockb lcov.info package-lock.json pnpm-lock.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json index 241108b..dfaf895 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,10 @@ { "[solidity]": { - "editor.defaultFormatter": "NomicFoundation.hardhat-solidity" + "editor.defaultFormatter": "JuanBlanco.solidity" }, "[toml]": { "editor.defaultFormatter": "tamasfe.even-better-toml" }, - "solidity.formatter": "forge" -} + "solidity.formatter": "forge", + "solidity.compileUsingRemoteVersion": "v0.8.25+commit.b61c2a91" +} \ No newline at end of file diff --git a/README.md b/README.md index f63c082..74eff1f 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,96 @@ -# Foundry Template [![Open in Gitpod][gitpod-badge]][gitpod] [![Github Actions][gha-badge]][gha] [![Foundry][foundry-badge]][foundry] [![License: MIT][license-badge]][license] +# Token Vendor Machine Challenge -[gitpod]: https://gitpod.io/#https://github.com/PaulRBerg/foundry-template -[gitpod-badge]: https://img.shields.io/badge/Gitpod-Open%20in%20Gitpod-FFB45B?logo=gitpod -[gha]: https://github.com/PaulRBerg/foundry-template/actions -[gha-badge]: https://github.com/PaulRBerg/foundry-template/actions/workflows/ci.yml/badge.svg -[foundry]: https://getfoundry.sh/ -[foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg -[license]: https://opensource.org/licenses/MIT -[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg +This project is designed to test your skills in smart contract development using Solidity. -A Foundry-based template for developing Solidity smart contracts, with sensible defaults. +## Project Overview -## What's Inside +You are tasked with implementing a token vendor machine that allows users to buy and sell tokens. The project consists of two main contracts: -- [Forge](https://github.com/foundry-rs/foundry/blob/master/forge): compile, test, fuzz, format, and deploy smart - contracts -- [Forge Std](https://github.com/foundry-rs/forge-std): collection of helpful contracts and utilities for testing -- [Prettier](https://github.com/prettier/prettier): code formatter for non-Solidity files -- [Solhint](https://github.com/protofire/solhint): linter for Solidity code +1. `BlockfulToken.sol`: An ERC20 token contract +2. `TokenVendor.sol`: A vendor contract for buying and selling tokens ## Getting Started -Click the [`Use this template`](https://github.com/PaulRBerg/foundry-template/generate) button at the top of the page to -create a new repository with this repo as the initial state. +To get started with this project, follow these steps: -Or, if you prefer to install the template manually: +1. Clone this repository. +2. Run `npm install` to install Node.js dependencies -```sh -$ mkdir my-project -$ cd my-project -$ forge init --template PaulRBerg/foundry-template -$ bun install # install Solhint, Prettier, and other Node.js deps -``` - -If this is your first time with Foundry, check out the -[installation](https://github.com/foundry-rs/foundry#installation) instructions. - -## Features - -This template builds upon the frameworks and libraries mentioned above, so please consult their respective documentation -for details about their specific features. - -For example, if you're interested in exploring Foundry in more detail, you should look at the -[Foundry Book](https://book.getfoundry.sh/). In particular, you may be interested in reading the -[Writing Tests](https://book.getfoundry.sh/forge/writing-tests.html) tutorial. - -### Sensible Defaults - -This template comes with a set of sensible default configurations for you to use. These defaults can be found in the -following files: - -```text -├── .editorconfig -├── .gitignore -├── .prettierignore -├── .prettierrc.yml -├── .solhint.json -├── foundry.toml -└── remappings.txt -``` - -### VSCode Integration - -This template is IDE agnostic, but for the best user experience, you may want to use it in VSCode alongside Nomic -Foundation's [Solidity extension](https://marketplace.visualstudio.com/items?itemName=NomicFoundation.hardhat-solidity). - -For guidance on how to integrate a Foundry project in VSCode, please refer to this -[guide](https://book.getfoundry.sh/config/vscode). - -### GitHub Actions - -This template comes with GitHub Actions pre-configured. Your contracts will be linted and tested on every push and pull -request made to the `main` branch. +## Your Tasks -You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). +1. Implement the `BlockfulToken` contract: + - It should be an ERC20 token with a name, symbol, and 18 decimals. + - Initial supply should be 1,000,000 tokens. -## Installing Dependencies +2. Implement the `TokenVendor` contract with the following functionality: + - Allow users to buy tokens with ETH (1 ETH = 100 tokens) + - Allow users to sell tokens back to the contract + - Allow the owner to withdraw ETH from the contract + - Implement proper access control (only owner can withdraw) + - Emit events for token purchases, sales, and ETH withdrawals -Foundry typically uses git submodules to manage dependencies, but this template uses Node.js packages because -[submodules don't scale](https://twitter.com/PaulRBerg/status/1736695487057531328). +3. Complete the test file `test/TokenVendor.t.sol`: + - We've provided some basic "happy path" tests + - Implement additional tests for edge cases and potential failure scenarios + - Aim for at least 90% test coverage -This is how to install dependencies: +4. Update this README with: + - Any additional setup or testing instructions + - An explanation of your design decisions + - Any potential improvements or considerations for a real-world deployment -1. Install the dependency using your preferred package manager, e.g. `bun install dependency-name` - - Use this syntax to install from GitHub: `bun install github:username/repo-name` -2. Add a remapping for the dependency in [remappings.txt](./remappings.txt), e.g. - `dependency-name=node_modules/dependency-name` +## Bonus Tasks -Note that OpenZeppelin Contracts is pre-installed, so you can follow that as an example. +If you complete the main tasks and want to demonstrate more advanced skills: -## Writing Tests - -To write a new test contract, you start by importing `Test` from `forge-std`, and then you inherit it in your test -contract. Forge Std comes with a pre-instantiated [cheatcodes](https://book.getfoundry.sh/cheatcodes/) environment -accessible via the `vm` property. If you would like to view the logs in the terminal output, you can add the `-vvv` flag -and use [console.log](https://book.getfoundry.sh/faq?highlight=console.log#how-do-i-use-consolelog). - -This template comes with an example test contract [Foo.t.sol](./test/Foo.t.sol) +1. Implement a simple dynamic pricing mechanism (e.g., price increases as supply decreases) +2. Add a feature to pause/unpause the contract (owner only) +3. Implement a whitelist system for early access to token sales ## Usage -This is a list of the most frequently needed commands. +Here are some common commands you'll need: ### Build -Build the contracts: - -```sh -$ forge build -``` - -### Clean - -Delete the build artifacts and cache directories: - ```sh -$ forge clean +npm run build ``` -### Compile - -Compile the contracts: +### Test ```sh -$ forge build +npm run test ``` ### Coverage -Get a test coverage report: - ```sh -$ forge coverage +npm run test:coverage ``` -### Deploy +## Evaluation Criteria -Deploy to Anvil: - -```sh -$ forge script script/Deploy.s.sol --broadcast --fork-url http://localhost:8545 -``` +Your submission will be evaluated based on: -For this script to work, you need to have a `MNEMONIC` environment variable set to a valid -[BIP39 mnemonic](https://iancoleman.io/bip39/). +- Correctness of the implementation +- Code quality and organization +- Test coverage and quality +- Security considerations +- Testnet deployment +- Documentation and comments +- (For bonus tasks) Creativity and effectiveness of additional features -For instructions on how to deploy to a testnet or mainnet, check out the -[Solidity Scripting](https://book.getfoundry.sh/tutorials/solidity-scripting.html) tutorial. +## Submission -### Format - -Format the contracts: - -```sh -$ forge fmt -``` - -### Gas Usage - -Get a gas report: - -```sh -$ forge test --gas-report -``` - -### Lint - -Lint the contracts: - -```sh -$ bun run lint -``` - -### Test - -Run the tests: - -```sh -$ forge test -``` - -Generate test coverage and output result to the terminal: - -```sh -$ bun run test:coverage -``` - -Generate test coverage with lcov report (you'll have to open the `./coverage/index.html` file in your browser, to do so -simply copy paste the path): - -```sh -$ bun run test:coverage:report -``` +Please submit your completed project as a Git repository. Make sure to include: -## Related Efforts +- All source code files +- Complete test suite +- Updated README with your notes and explanations -- [abigger87/femplate](https://github.com/abigger87/femplate) -- [cleanunicorn/ethereum-smartcontract-template](https://github.com/cleanunicorn/ethereum-smartcontract-template) -- [foundry-rs/forge-template](https://github.com/foundry-rs/forge-template) -- [FrankieIsLost/forge-template](https://github.com/FrankieIsLost/forge-template) +## Note -## License +This project uses [Foundry](https://getfoundry.sh/). If you're new to Foundry, check out the [Foundry Book](https://book.getfoundry.sh/) for detailed instructions and tutorials. -This project is licensed under MIT. +Good luck with the challenge! We're excited to see your implementation. diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 86d5c93cd74609bc9ea3f2465af343852a58d7c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28441 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 diff --git a/foundry.toml b/foundry.toml index dacd6da..524153e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,55 +1,48 @@ # Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config [profile.default] - auto_detect_solc = false - block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT - bytecode_hash = "none" - evm_version = "shanghai" - fuzz = { runs = 1_000 } - gas_reports = ["*"] - optimizer = true - optimizer_runs = 10_000 - out = "out" - script = "script" - solc = "0.8.25" - src = "src" - test = "test" +auto_detect_solc = false +block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT +bytecode_hash = "none" +evm_version = "shanghai" +fuzz = { runs = 1_000 } +gas_reports = ["*"] +optimizer = true +optimizer_runs = 10_000 +out = "out" +script = "script" +solc = "0.8.25" +src = "src" +test = "test" [profile.ci] - fuzz = { runs = 10_000 } - verbosity = 4 +fuzz = { runs = 10_000 } +verbosity = 4 [etherscan] - arbitrum = { key = "${API_KEY_ARBISCAN}" } - avalanche = { key = "${API_KEY_SNOWTRACE}" } - base = { key = "${API_KEY_BASESCAN}" } - bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } - gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } - goerli = { key = "${API_KEY_ETHERSCAN}" } - mainnet = { key = "${API_KEY_ETHERSCAN}" } - optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } - polygon = { key = "${API_KEY_POLYGONSCAN}" } - sepolia = { key = "${API_KEY_ETHERSCAN}" } +goerli = { key = "${API_KEY_ETHERSCAN}" } +mainnet = { key = "${API_KEY_ETHERSCAN}" } +sepolia = { key = "${API_KEY_ETHERSCAN}" } [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 +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] - arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" - avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" - base = "https://mainnet.base.org" - bnb_smart_chain = "https://bsc-dataseed.binance.org" - gnosis_chain = "https://rpc.gnosischain.com" - goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" - localhost = "http://localhost:8545" - mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" - optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" - polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" - sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" +arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" +avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" +base = "https://mainnet.base.org" +bnb_smart_chain = "https://bsc-dataseed.binance.org" +gnosis_chain = "https://rpc.gnosischain.com" +goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" +localhost = "http://localhost:8545" +mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" +optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" +polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" +sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" diff --git a/package.json b/package.json index 8c8d160..c706ba8 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "@openzeppelin/contracts": "^5.0.1" }, "devDependencies": { - "forge-std": "github:foundry-rs/forge-std#v1.8.1", + "forge-std": "github:foundry-rs/forge-std", + "@openzeppelin/contracts": "^5.0.2", "prettier": "^3.0.0", "solhint": "^3.6.2" }, @@ -27,12 +28,13 @@ "scripts": { "clean": "rm -rf cache out", "build": "forge build", - "lint": "bun run lint:sol && bun run prettier:check", - "lint:sol": "forge fmt --check && bun solhint \"{script,src,test}/**/*.sol\"", + "lint": "npm run lint:sol && npm run prettier:check", + "lint:sol": "forge fmt --check && npm solhint \"{script,src,test}/**/*.sol\"", "prettier:check": "prettier --check \"**/*.{json,md,yml}\" --ignore-path \".prettierignore\"", "prettier:write": "prettier --write \"**/*.{json,md,yml}\" --ignore-path \".prettierignore\"", "test": "forge test", "test:coverage": "forge coverage", - "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage" + "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage", + "deploy": "forge script script/Deploy.s.sol:Deploy --rpc-url http://localhost:8545 --broadcast" } -} +} \ No newline at end of file diff --git a/remappings.txt b/remappings.txt index 550f908..e755329 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,2 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ -forge-std/=node_modules/forge-std/ +forge-std/=node_modules/forge-std/src diff --git a/script/Base.s.sol b/script/Base.s.sol index 07135a2..7a28f61 100644 --- a/script/Base.s.sol +++ b/script/Base.s.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.25 <0.9.0; -import { Script } from "forge-std/src/Script.sol"; +import { Script } from "forge-std/Script.sol"; abstract contract BaseScript is Script { /// @dev Included to enable compilation of the script without a $MNEMONIC environment variable. diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 498db52..cf381de 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.25 <0.9.0; -import { Foo } from "../src/Foo.sol"; +import { BlockfulToken } from "../src/BlockfulToken.sol"; +import { TokenVendor } from "../src/TokenVendor.sol"; import { BaseScript } from "./Base.s.sol"; /// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting contract Deploy is BaseScript { - function run() public broadcast returns (Foo foo) { - foo = new Foo(); + function run() public broadcast returns (BlockfulToken token, TokenVendor vendor) { + token = new BlockfulToken(100 ether); + vendor = new TokenVendor(address(token), 0.001 ether); } } diff --git a/src/BlockfulToken.sol b/src/BlockfulToken.sol new file mode 100644 index 0000000..6e26974 --- /dev/null +++ b/src/BlockfulToken.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.25; + +contract BlockfulToken { + constructor(uint256 initialSupply) { } + + function transfer(address to, uint256 amount) public { + // Implement transfer logic + } +} diff --git a/src/Foo.sol b/src/Foo.sol deleted file mode 100644 index 7483070..0000000 --- a/src/Foo.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.25; - -contract Foo { - function id(uint256 value) external pure returns (uint256) { - return value; - } -} diff --git a/src/TokenVendor.sol b/src/TokenVendor.sol new file mode 100644 index 0000000..3bb4593 --- /dev/null +++ b/src/TokenVendor.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.25; + +contract TokenVendor { + event TokensPurchased(address buyer, uint256 amountOfETH, uint256 amountOfTokens); + event TokensSold(address seller, uint256 amountOfTokens, uint256 amountOfETH); + event EthWithdrawn(address owner, uint256 amount); + + constructor(address _token, uint256 _tokenPrice) { } + + function buyTokens() public payable { + // Implement buying logic + } + + function sellTokens(uint256 _amount) public { + // Implement selling logic + } + + function withdraw() public { + // Implement withdrawal logic + } + + // Add any additional functions here +} diff --git a/test/Foo.t.sol b/test/Foo.t.sol deleted file mode 100644 index 727337a..0000000 --- a/test/Foo.t.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.25 <0.9.0; - -import { Test } from "forge-std/src/Test.sol"; -import { console2 } from "forge-std/src/console2.sol"; - -import { Foo } from "../src/Foo.sol"; - -interface IERC20 { - function balanceOf(address account) external view returns (uint256); -} - -/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book: -/// https://book.getfoundry.sh/forge/writing-tests -contract FooTest is Test { - Foo internal foo; - - /// @dev A function invoked before each test case is run. - function setUp() public virtual { - // Instantiate the contract-under-test. - foo = new Foo(); - } - - /// @dev Basic test. Run it with `forge test -vvv` to see the console log. - function test_Example() external view { - console2.log("Hello World"); - uint256 x = 42; - assertEq(foo.id(x), x, "value mismatch"); - } - - /// @dev Fuzz test that provides random values for an unsigned integer, but which rejects zero as an input. - /// If you need more sophisticated input validation, you should use the `bound` utility instead. - /// See https://twitter.com/PaulRBerg/status/1622558791685242880 - function testFuzz_Example(uint256 x) external view { - vm.assume(x != 0); // or x = bound(x, 1, 100) - assertEq(foo.id(x), x, "value mismatch"); - } - - /// @dev Fork test that runs against an Ethereum Mainnet fork. For this to work, you need to set `API_KEY_ALCHEMY` - /// in your environment You can get an API key for free at https://alchemy.com. - function testFork_Example() external { - // Silently pass this test if there is no API key. - string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string("")); - if (bytes(alchemyApiKey).length == 0) { - return; - } - - // Otherwise, run the test against the mainnet fork. - vm.createSelectFork({ urlOrAlias: "mainnet", blockNumber: 16_428_000 }); - address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address holder = 0x7713974908Be4BEd47172370115e8b1219F4A5f0; - uint256 actualBalance = IERC20(usdc).balanceOf(holder); - uint256 expectedBalance = 196_307_713.810457e6; - assertEq(actualBalance, expectedBalance); - } -} diff --git a/test/TokenVendor.t.sol b/test/TokenVendor.t.sol new file mode 100644 index 0000000..bd60991 --- /dev/null +++ b/test/TokenVendor.t.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import { Test } from "forge-std/Test.sol"; + +import { TokenVendor } from "../src/TokenVendor.sol"; +import { BlockfulToken } from "../src/BlockfulToken.sol"; + +contract TokenVendorTest is Test { + TokenVendor public vendor; + BlockfulToken public token; + address public owner; + address public user1; + uint256 public constant INITIAL_SUPPLY = 1_000_000 * 10 ** 18; + uint256 public constant TOKEN_PRICE = 100; // 1 ETH = 100 tokens + + function setUp() public { + owner = address(this); + user1 = address(0x1); + token = new BlockfulToken(INITIAL_SUPPLY); + vendor = new TokenVendor(address(token), TOKEN_PRICE); + token.transfer(address(vendor), INITIAL_SUPPLY); + } + + function testBuyTokens() public { + uint256 ethAmount = 1 ether; + uint256 expectedTokens = ethAmount * TOKEN_PRICE; + + // Add ETH to the user + vm.deal(user1, ethAmount); + // Impersonate the user + vm.prank(user1); + vendor.buyTokens{ value: ethAmount }(); + + assertEq(token.balanceOf(user1), expectedTokens); + assertEq(address(vendor).balance, ethAmount); + } + + function testSellTokens() public { + uint256 tokenAmount = 100 * 10 ** 18; + uint256 expectedEth = tokenAmount / TOKEN_PRICE; + + // First, buy some tokens + vm.deal(user1, 1 ether); + vm.startPrank(user1); + vendor.buyTokens{ value: 1 ether }(); + + // Approve vendor to spend tokens + token.approve(address(vendor), tokenAmount); + + // Sell tokens + uint256 initialBalance = user1.balance; + vendor.sellTokens(tokenAmount); + vm.stopPrank(); + + assertEq(token.balanceOf(user1), 0); + assertEq(user1.balance - initialBalance, expectedEth); + } + + function testWithdraw() public { + uint256 ethAmount = 1 ether; + + // First, buy some tokens to add ETH to the vendor + vm.deal(user1, ethAmount); + vm.prank(user1); + vendor.buyTokens{ value: ethAmount }(); + + // Withdraw ETH + uint256 initialBalance = owner.balance; + vendor.withdraw(); + + assertEq(owner.balance - initialBalance, ethAmount); + assertEq(address(vendor).balance, 0); + } + + function testBuyTokensEvent() public { + uint256 ethAmount = 1 ether; + uint256 expectedTokens = ethAmount * TOKEN_PRICE; + + vm.deal(user1, ethAmount); + vm.prank(user1); + + vm.expectEmit(true, false, false, true); + emit TokenVendor.TokensPurchased(user1, ethAmount, expectedTokens); + vendor.buyTokens{ value: ethAmount }(); + } + + function testSellTokensEvent() public { + uint256 tokenAmount = 100 * 10 ** 18; + uint256 expectedEth = tokenAmount / TOKEN_PRICE; + + // First, buy some tokens + vm.deal(user1, 1 ether); + vm.startPrank(user1); + vendor.buyTokens{ value: 1 ether }(); + + // Approve vendor to spend tokens + token.approve(address(vendor), tokenAmount); + + // Sell tokens + vm.expectEmit(true, false, false, true); + emit TokenVendor.TokensSold(user1, tokenAmount, expectedEth); + vendor.sellTokens(tokenAmount); + vm.stopPrank(); + } + + function testWithdrawEvent() public { + uint256 ethAmount = 1 ether; + + // First, buy some tokens to add ETH to the vendor + vm.deal(user1, ethAmount); + vm.prank(user1); + vendor.buyTokens{ value: ethAmount }(); + + // Withdraw ETH + vm.expectEmit(true, false, false, true); + emit TokenVendor.EthWithdrawn(owner, ethAmount); + vendor.withdraw(); + } +}