Skip to content
Open
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
38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

permissions: {}

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
name: Foundry project
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
submodules: recursive

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

- name: Show Forge version
run: forge --version

- name: Run Forge fmt
run: forge fmt --check

- name: Run Forge build
run: forge build --sizes

- name: Run Forge tests
run: forge test -vvv
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

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

# Docs
docs/

# Dotenv file
.env
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
[submodule "wk3- Data Types/day-4/lib/forge-std"]
path = wk3- Data Types/day-4/lib/forge-std
url = https://github.com/foundry-rs/forge-std.git
[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
66 changes: 66 additions & 0 deletions 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
```
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.16.2",
"rev": "bf647bd6046f2f7da30d0c2bf435e5c76a780c1b"
}
},
"lib/openzeppelin-contracts": {
"tag": {
"name": "v5.7.0",
"rev": "cab19933c33c2ad1d4c7a84864a3601dddfd16f3"
}
}
}
10 changes: 10 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = [
"forge-std/=lib/forge-std/src/",
"@openzeppelin/=lib/openzeppelin-contracts/"
]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1 change: 1 addition & 0 deletions lib/forge-std
Submodule forge-std added at bf647b
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at cab199
19 changes: 19 additions & 0 deletions script/Counter.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";

contract CounterScript is Script {
Counter public counter;

function setUp() public {}

function run() public {
vm.startBroadcast();

counter = new Counter();

vm.stopBroadcast();
}
}
14 changes: 14 additions & 0 deletions src/Counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

contract Counter {
uint256 public number;

function setNumber(uint256 newNumber) public {
number = newNumber;
}

function increment() public {
number++;
}
}
59 changes: 59 additions & 0 deletions src/StakingNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract StakingNFT is ERC721 {

address public stakingContract;

uint256 public nextTokenId = 1;

// tier (1, 2 or 3) for each token
mapping(uint256 => uint8) public tokenTier;

// metadata URLs per tier: Bronze, Silver, Gold
string public bronzeURI = "ipfs://YOUR_BRONZE_CID/bronze.json";
string public silverURI = "ipfs://YOUR_SILVER_CID/silver.json";
string public goldURI = "ipfs://YOUR_GOLD_CID/gold.json";

constructor(address _stakingContract) ERC721("StakingNFT", "SNFT") {
stakingContract = _stakingContract;
}

// returns the metadata URL for the token based on its tier
function tokenURI(uint256 tokenId) public view override returns (string memory) {
require(ownerOf(tokenId) != address(0), "Token does not exist");

uint8 tier = tokenTier[tokenId];

if (tier == 3) {
return goldURI;
} else if (tier == 2) {
return silverURI;
} else {
return bronzeURI;
}
}

// mints a tier NFT to the user, only the staking contract can call this
function mint(address user, uint8 tier) external returns (uint256) {
require(msg.sender == stakingContract, "Only the staking contract can mint");

uint256 tokenId = nextTokenId;
nextTokenId++;

_mint(user, tokenId);
tokenTier[tokenId] = tier;

return tokenId;
}

// burns the NFT when the user withdraws, only the staking contract can call this
function burn(uint256 tokenId) external {
require(msg.sender == stakingContract, "Only the staking contract can burn");

_burn(tokenId);
delete tokenTier[tokenId];
}
}
106 changes: 106 additions & 0 deletions src/TieredStaking.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {StakingNFT} from "./StakingNFT.sol";

contract TieredStaking {

StakingNFT public nft;

// receives the 10% penalty on emergency withdrawals
address public owner;

// one stake per user
struct Stake {
uint256 amount; // ETH deposited
uint256 lockedUntil; // timestamp when lock expires
uint256 nftTokenId; // receipt NFT id
uint8 tier; // 1 = Bronze, 2 = Silver, 3 = Gold
}

mapping(address => Stake) public stakes;

event Staked(address indexed user, uint256 amount, uint8 tier, uint256 lockedUntil);
event Withdrawn(address indexed user, uint256 amount);
event EmergencyWithdrawn(address indexed user, uint256 amountReturned, uint256 penaltyPaid);

// payable so the deployer can seed the contract with ETH at deploy time
constructor() payable {
owner = msg.sender;
nft = new StakingNFT(address(this));
}

// deposit ETH and choose a lock duration in seconds
function stake(uint256 lockDurationInSeconds) external payable {
require(msg.value > 0, "You must send some ETH to stake");
require(lockDurationInSeconds >= 1 days, "Minimum lock is 1 day");
require(lockDurationInSeconds <= 730 days, "Maximum lock is 730 days");
require(stakes[msg.sender].amount == 0, "Withdraw your current stake first");

// assign tier based on amount sent
uint8 tier;
if (msg.value >= 50 ether) {
tier = 3; // Gold
} else if (msg.value >= 10 ether) {
tier = 2; // Silver
} else {
tier = 1; // Bronze
}

uint256 lockedUntil = block.timestamp + lockDurationInSeconds;

// mint the receipt NFT to the user
uint256 nftTokenId = nft.mint(msg.sender, tier);

stakes[msg.sender] = Stake({
amount: msg.value,
lockedUntil: lockedUntil,
nftTokenId: nftTokenId,
tier: tier
});

emit Staked(msg.sender, msg.value, tier, lockedUntil);
}

// withdraw after lock expires - burns NFT, returns 100% ETH
function withdraw() external {
Stake memory myStake = stakes[msg.sender];

require(myStake.amount > 0, "No active stake");
require(block.timestamp >= myStake.lockedUntil, "Stake is still locked");

// clear state before sending ETH to prevent re-entrancy
delete stakes[msg.sender];

nft.burn(myStake.nftTokenId);

(bool sent, ) = msg.sender.call{value: myStake.amount}("");
require(sent, "ETH transfer failed");

emit Withdrawn(msg.sender, myStake.amount);
}

// withdraw anytime but lose 10% as a penalty
function emergencyWithdraw() external {
Stake memory myStake = stakes[msg.sender];

require(myStake.amount > 0, "No active stake");

// clear state before sending ETH to prevent re-entrancy
delete stakes[msg.sender];

nft.burn(myStake.nftTokenId);

uint256 penaltyAmount = (myStake.amount * 10) / 100;
uint256 amountToReturn = myStake.amount - penaltyAmount;

(bool sentToUser, ) = msg.sender.call{value: amountToReturn}("");
require(sentToUser, "ETH transfer failed");

// penalty goes to the owner
(bool sentToOwner, ) = owner.call{value: penaltyAmount}("");
require(sentToOwner, "Penalty transfer failed");

emit EmergencyWithdrawn(msg.sender, amountToReturn, penaltyAmount);
}
}
24 changes: 24 additions & 0 deletions test/Counter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

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

contract CounterTest is Test {
Counter public counter;

function setUp() public {
counter = new Counter();
counter.setNumber(0);
}

function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1);
}

function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
Loading