diff --git a/.gitmodules b/.gitmodules index 9c2484199..ceb0d21ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -67,3 +67,6 @@ [submodule "dispute-murky"] path = protocol-units/dispute/lib/murky url = https://github.com/dmfxyz/murky +[submodule "protocol-units/tokens/mock/testnet/holesky/lib/openzeppelin-contracts"] + path = protocol-units/tokens/mock/testnet/holesky/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/protocol-units/tokens/mock/testnet/holesky/.github/workflows/test.yml b/protocol-units/tokens/mock/testnet/holesky/.github/workflows/test.yml new file mode 100644 index 000000000..762a2966f --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/protocol-units/tokens/mock/testnet/holesky/.gitignore b/protocol-units/tokens/mock/testnet/holesky/.gitignore new file mode 100644 index 000000000..85198aaa5 --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/protocol-units/tokens/mock/testnet/holesky/README.md b/protocol-units/tokens/mock/testnet/holesky/README.md new file mode 100644 index 000000000..9265b4558 --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/README.md @@ -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 --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/protocol-units/tokens/mock/testnet/holesky/foundry.toml b/protocol-units/tokens/mock/testnet/holesky/foundry.toml new file mode 100644 index 000000000..25b918f9c --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/protocol-units/tokens/mock/testnet/holesky/lib/openzeppelin-contracts b/protocol-units/tokens/mock/testnet/holesky/lib/openzeppelin-contracts new file mode 160000 index 000000000..dbb6104ce --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/protocol-units/tokens/mock/testnet/holesky/remappings.txt b/protocol-units/tokens/mock/testnet/holesky/remappings.txt new file mode 100644 index 000000000..ec77bb23a --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/remappings.txt @@ -0,0 +1,5 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +ds-test/=lib/openzeppelin-contracts/lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/openzeppelin-contracts/lib/forge-std/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ diff --git a/protocol-units/tokens/mock/testnet/holesky/src/MOVEFaucet.sol b/protocol-units/tokens/mock/testnet/holesky/src/MOVEFaucet.sol new file mode 100644 index 000000000..96bb37403 --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/src/MOVEFaucet.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IERC20 { + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 value) external returns (bool); + function decimals() external view returns (uint8); +} + +contract MOVEFaucet { + + IERC20 public move; + uint256 public rateLimit = 1 days; + uint256 public amount = 10; + uint256 public maxBalance = 1; + address public owner; + mapping(address => uint256) public lastFaucetClaim; + + constructor(IERC20 _move) { + move = _move; + owner = msg.sender; + } + + function faucet() external payable { + require(msg.value == 10 ** 17, "MOVEFaucet: eth invalid amount"); + require(move.balanceOf(msg.sender) < maxBalance * 10 ** move.decimals(), "MOVEFaucet: balance must be less than determined amount of MOVE"); + require(block.timestamp - lastFaucetClaim[msg.sender] >= rateLimit, "MOVEFaucet: rate limit exceeded"); + lastFaucetClaim[msg.sender] = block.timestamp; + require(move.transfer(msg.sender, amount * 10 ** move.decimals()), "MOVEFaucet: transfer failed"); + } + + function setConfig(uint256 _rateLimit, uint256 _amount, uint256 _maxBalance, address _owner) external { + require(msg.sender == owner, "MOVEFaucet: only owner can set config"); + rateLimit = _rateLimit; + amount = _amount; + maxBalance = _maxBalance; + owner = _owner; + + } + + function withdraw() external { + require(msg.sender == owner, "MOVEFaucet: only owner can retrieve funds"); + (bool status,) = owner.call{value: address(this).balance}(""); + require(status == true, "error during transaction"); + } +} \ No newline at end of file diff --git a/protocol-units/tokens/mock/testnet/holesky/test/Faucet.t.sol b/protocol-units/tokens/mock/testnet/holesky/test/Faucet.t.sol new file mode 100644 index 000000000..f9a49dca7 --- /dev/null +++ b/protocol-units/tokens/mock/testnet/holesky/test/Faucet.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {MOVEFaucet, IERC20} from '../src/MOVEFaucet.sol'; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract MOVETokenDev is ERC20, AccessControl { + + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + bytes32 public constant MINTER_ADMIN_ROLE = keccak256("MINTER_ADMIN_ROLE"); + + /** + * @dev Initialize the contract + */ + constructor(address manager) ERC20("Movement", "MOVE") { + _mint(manager, 10000000000 * 10 ** decimals()); + _grantRole(MINTER_ADMIN_ROLE, manager); + _grantRole(MINTER_ROLE, manager); + } + + function grantRoles(address account) public onlyRole(DEFAULT_ADMIN_ROLE) { + _grantRole(MINTER_ADMIN_ROLE, account); + _grantRole(MINTER_ROLE, account); + + } + + function decimals() public pure override returns (uint8) { + return 8; + } +} + +contract MOVEFaucetTest is Test { + MOVEFaucet public faucet; + MOVETokenDev public token; + + fallback() external payable {} + + function setUp() public { + token = new MOVETokenDev(address(this)); + faucet = new MOVEFaucet(IERC20(address(token))); + } + + function testFaucet() public { + vm.warp(1 days); + + token.balanceOf(address(this)); + + token.transfer(address(faucet), 20 * 10 ** token.decimals()); + + vm.deal(address(0x1337), 2* 10**17); + + vm.startPrank(address(0x1337)); + vm.expectRevert("MOVEFaucet: eth invalid amount"); + faucet.faucet{value: 10**16}(); + + faucet.faucet{value: 10**17}(); + assertEq(token.balanceOf(address(0x1337)), 10 * 10 ** token.decimals()); + + vm.expectRevert("MOVEFaucet: balance must be less than determined amount of MOVE"); + faucet.faucet{value: 10**17}(); + + token.transfer(address(0xdead), token.balanceOf(address(0x1337))); + + vm.expectRevert("MOVEFaucet: rate limit exceeded"); + faucet.faucet{value: 10**17}(); + + vm.warp(block.timestamp + 1 days); + faucet.faucet{value: 10**17}(); + vm.stopPrank(); + vm.prank(address(this)); + uint256 balance = address(this).balance; + faucet.withdraw(); + assertEq(address(faucet).balance, 0); + assertEq(address(this).balance, balance + 2*10**17); + } + + +} \ No newline at end of file