Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Deployment
PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
OWNER_ADDRESS=0x0000000000000000000000000000000000000000

# RPC endpoints
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY
OPTIMISM_RPC_URL=https://opt-mainnet.g.alchemy.com/v2/YOUR_KEY
BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY

# Block explorers (verification)
ETHERSCAN_API_KEY=
ARBISCAN_API_KEY=
OPTIMISTIC_ETHERSCAN_API_KEY=
BASESCAN_API_KEY=
19 changes: 19 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Force LF line endings for all text files, regardless of platform.
# Prevents Windows CRLF mess when collaborating with Linux/Mac contributors.
* text=auto eol=lf

# Solidity & related
*.sol text eol=lf
*.toml text eol=lf
*.md text eol=lf
*.txt text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf

# Binary files - never touch
*.png binary
*.jpg binary
*.pdf binary
*.zip binary
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Foundry build artifacts
cache/
out/
broadcast/

# Environment
.env
.env.local
.env.*.local

# OS
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/
*.swp
*.swo

# Coverage
lcov.info
coverage/
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,57 @@
# Augur Lituus oracle

- Solidity `^0.8.35`
- Git submodules for dependencies
- Unit + fuzz + invariant tests included as a working reference

## Setup

```bash
git clone --recurse-submodules <repo> && cd <repo>
cp .env.example .env
forge build
forge test
```

If you forgot `--recurse-submodules` on clone:

```bash
git submodule update --init --recursive
```

## Common commands

```bash
forge build # compile
forge test # run all tests
forge test -vvv # verbose, with traces on failure
forge coverage # coverage report
forge fmt # format
forge snapshot # gas snapshot

FOUNDRY_PROFILE=ci forge test # 10k fuzz runs
FOUNDRY_PROFILE=deep forge test # 100k fuzz runs, deep invariants
```

## Deploy

```bash
# local
anvil # terminal 1
forge script script/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast

# testnet / mainnet (RPC aliases defined in foundry.toml)
forge script script/Deploy.s.sol --rpc-url sepolia --broadcast --verify
```

## Layout

```
src/ contracts
test/
unit/ deterministic scenarios
fuzz/ property-based
invariant/ stateful, via a handler
script/ deploy scripts
lib/ submodule dependencies
```
14 changes: 14 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"lib\\forge-std": {
"tag": {
"name": "v1.14.0",
"rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6"
}
},
"lib\\openzeppelin-contracts": {
"tag": {
"name": "v5.6.1",
"rev": "5fd1781b1454fd1ef8e722282f86f9293cacf256"
}
}
}
55 changes: 55 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Full reference: https://github.com/foundry-rs/foundry/tree/master/config

[profile.default]
solc = "0.8.35"
src = "src"
out = "out"
test = "test"
script = "script"
libs = ["lib"]
optimizer = true
optimizer_runs = 200
via_ir = false
bytecode_hash = "none" # reproducible bytecode
cbor_metadata = false
gas_reports = ["*"]
fs_permissions = [{ access = "read-write", path = "./broadcast" }]
ignored_warnings_from = ["lib"] # ignoring warnings from external packages

fuzz = { runs = 256 }
invariant = { runs = 256, depth = 15, fail_on_revert = false, call_override = false }

# Used in CI, heavier testing, fail fast
[profile.ci]
fuzz = { runs = 10_000 }
invariant = { runs = 1_000, depth = 50, fail_on_revert = false }
verbosity = 3

# Run locally before a release, exhaustive
[profile.deep]
fuzz = { runs = 100_000 }
invariant = { runs = 5_000, depth = 100, fail_on_revert = false }

[fmt]
line_length = 120
tab_width = 4
bracket_spacing = true
int_types = "long"
multiline_func_header = "all"
quote_style = "double"
number_underscore = "thousands"
wrap_comments = true

[rpc_endpoints]
mainnet = "${MAINNET_RPC_URL}"
sepolia = "${SEPOLIA_RPC_URL}"
arbitrum = "${ARBITRUM_RPC_URL}"
optimism = "${OPTIMISM_RPC_URL}"
base = "${BASE_RPC_URL}"

[etherscan]
mainnet = { key = "${ETHERSCAN_API_KEY}" }
sepolia = { key = "${ETHERSCAN_API_KEY}" }
arbitrum = { key = "${ARBISCAN_API_KEY}" }
optimism = { key = "${OPTIMISTIC_ETHERSCAN_API_KEY}" }
base = { key = "${BASESCAN_API_KEY}" }
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at 1801b0
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at 5fd178
2 changes: 2 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
forge-std/=lib/forge-std/src/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
23 changes: 23 additions & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.35;

import { Script, console } from "forge-std/Script.sol";

import { Counter } from "src/Counter.sol";

/// @notice Deployment script for Counter.
contract Deploy is Script {
function run() external returns (Counter counter) {
uint256 deployerKey = vm.envUint("PRIVATE_KEY");
address deployer = vm.addr(deployerKey);
address owner = vm.envOr("OWNER_ADDRESS", deployer);

vm.startBroadcast(deployerKey);
counter = new Counter(owner);
vm.stopBroadcast();

console.log("Counter deployed at :", address(counter));
console.log("Owner set to :", owner);
console.log("Deployer :", deployer);
}
}
83 changes: 83 additions & 0 deletions src/Counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.35;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

/// @title Counter
/// @notice Minimal counter demonstrating template patterns: OZ integration,
/// custom errors, indexed events, NatSpec, and bounded state.
contract Counter is Ownable {
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/

/// @notice Current counter value.
uint256 public number;

/*//////////////////////////////////////////////////////////////
CONSTANTS
//////////////////////////////////////////////////////////////*/

/// @notice Hard upper bound. Anything above reverts.
uint256 public constant MAX_NUMBER = type(uint128).max;

/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/

/// @notice Emitted when `number` is incremented by one.
/// @param by Caller that triggered the increment.
/// @param newValue Value after the increment.
event Incremented(address indexed by, uint256 newValue);

/// @notice Emitted when `number` is set to an arbitrary value.
/// @param by Caller that triggered the set.
/// @param newValue Value after the set.
event NumberSet(address indexed by, uint256 newValue);

/// @notice Emitted when `number` is reset to zero.
/// @param by Owner that triggered the reset.
event Reset(address indexed by);

/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/

/// @notice Thrown when an operation would push `number` above MAX_NUMBER.
error CounterOverflow();

/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/

/// @param initialOwner Address granted exclusive reset rights.
constructor(address initialOwner) Ownable(initialOwner) { }

/*//////////////////////////////////////////////////////////////
EXTERNAL WRITES
//////////////////////////////////////////////////////////////*/

/// @notice Increment `number` by one.
/// @dev Reverts with {CounterOverflow} if `number` already at MAX_NUMBER.
function increment() external {
if (number >= MAX_NUMBER) revert CounterOverflow();
unchecked {
++number;
}
emit Incremented(msg.sender, number);
}

/// @notice Set `number` to a specific value.
/// @param newNumber Target value, must be <= MAX_NUMBER.
function setNumber(uint256 newNumber) external {
if (newNumber > MAX_NUMBER) revert CounterOverflow();
number = newNumber;
emit NumberSet(msg.sender, newNumber);
}

/// @notice Reset the counter to zero. Owner-only.
function reset() external onlyOwner {
number = 0;
emit Reset(msg.sender);
}
}
53 changes: 53 additions & 0 deletions test/fuzz/Counter.fuzz.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.35;

import { Test } from "forge-std/Test.sol";

import { Counter } from "src/Counter.sol";

/// @notice Property-based tests. The fuzzer throws random inputs at the assumptions.
contract CounterFuzzTest is Test {
Counter internal counter;
address internal owner = makeAddr("owner");

function setUp() public {
counter = new Counter(owner);
}

/// @dev Property: any value in valid range round-trips through `setNumber`.
function testFuzz_SetNumber_ValidRange(uint256 x) public {
x = bound(x, 0, counter.MAX_NUMBER());

counter.setNumber(x);

assertEq(counter.number(), x);
}

/// @dev Property: any value above MAX_NUMBER must revert with CounterOverflow.
function testFuzz_SetNumber_RevertsAboveMax(uint256 x) public {
x = bound(x, counter.MAX_NUMBER() + 1, type(uint256).max);

vm.expectRevert(Counter.CounterOverflow.selector);
counter.setNumber(x);
}

/// @dev Property: N successive increments produce `number == N`.
function testFuzz_Increment_NTimes(uint8 n) public {
for (uint256 i = 0; i < n; ++i) {
counter.increment();
}
assertEq(counter.number(), n);
}

/// @dev Property: only `owner` can reset, regardless of caller.
function testFuzz_Reset_OnlyOwner(address caller) public {
vm.assume(caller != owner);
counter.setNumber(1);

vm.prank(caller);
vm.expectRevert();
counter.reset();

assertEq(counter.number(), 1);
}
}
Loading