Skip to content

Commit 29feb36

Browse files
first commit
0 parents  commit 29feb36

9 files changed

Lines changed: 295 additions & 0 deletions

File tree

.github/workflows/test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
permissions: {}
4+
5+
on:
6+
push:
7+
pull_request:
8+
workflow_dispatch:
9+
10+
jobs:
11+
check:
12+
name: Foundry project
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
steps:
17+
- uses: actions/checkout@v6
18+
with:
19+
persist-credentials: false
20+
submodules: recursive
21+
22+
- name: Install Foundry
23+
uses: foundry-rs/foundry-toolchain@v1
24+
25+
- name: Show Forge version
26+
run: forge --version
27+
28+
- name: Run Forge fmt
29+
run: forge fmt --check
30+
31+
- name: Run Forge build
32+
run: forge build --sizes
33+
34+
- name: Run Forge tests
35+
run: forge test -vvv

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Compiler files
2+
cache/
3+
out/
4+
5+
# Ignores development broadcast logs
6+
!/broadcast
7+
/broadcast/*/31337/
8+
/broadcast/**/dry-run/
9+
10+
# Docs
11+
docs/
12+
13+
# Dotenv file
14+
.env

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[submodule "lib/forge-std"]
2+
path = lib/forge-std
3+
url = https://github.com/foundry-rs/forge-std
4+
[submodule "lib/openzeppelin-contracts"]
5+
path = lib/openzeppelin-contracts
6+
url = https://github.com/Openzeppelin/openzeppelin-contracts

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
## Foundry
2+
3+
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
4+
5+
Foundry consists of:
6+
7+
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
8+
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
9+
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
10+
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
11+
12+
## Documentation
13+
14+
https://book.getfoundry.sh/
15+
16+
## Usage
17+
18+
### Build
19+
20+
```shell
21+
$ forge build
22+
```
23+
24+
### Test
25+
26+
```shell
27+
$ forge test
28+
```
29+
30+
### Format
31+
32+
```shell
33+
$ forge fmt
34+
```
35+
36+
### Gas Snapshots
37+
38+
```shell
39+
$ forge snapshot
40+
```
41+
42+
### Anvil
43+
44+
```shell
45+
$ anvil
46+
```
47+
48+
### Deploy
49+
50+
```shell
51+
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
52+
```
53+
54+
### Cast
55+
56+
```shell
57+
$ cast <subcommand>
58+
```
59+
60+
### Help
61+
62+
```shell
63+
$ forge --help
64+
$ anvil --help
65+
$ cast --help
66+
```

foundry.lock

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"lib\\forge-std": {
3+
"tag": {
4+
"name": "v1.15.0",
5+
"rev": "0844d7e1fc5e60d77b68e469bff60265f236c398"
6+
}
7+
},
8+
"lib\\openzeppelin-contracts": {
9+
"tag": {
10+
"name": "v5.5.0",
11+
"rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565"
12+
}
13+
}
14+
}

foundry.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[profile.default]
2+
src = "src"
3+
out = "out"
4+
libs = ["lib"]
5+
6+
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

lib/forge-std

Submodule forge-std added at 0844d7e

lib/openzeppelin-contracts

Submodule openzeppelin-contracts added at fcbae53

src/PSC_Token.sol

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.27;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import "@openzeppelin/contracts/access/Ownable.sol";
6+
import "@openzeppelin/contracts/finance/VestingWallet.sol";
7+
8+
/// @title Paradaise Supply Chain Token (PSC)
9+
/// @notice ERC20 token with team linear vesting and investor step vesting
10+
contract MyToken is ERC20, Ownable {
11+
// =========================
12+
// ======== CONSTANTS ======
13+
// =========================
14+
15+
uint256 public constant TOTAL_SUPPLY = 20_000_000 ether;
16+
uint256 public constant PUBLIC_PERCENT = 10;
17+
uint256 public constant TEAM_PERCENT = 3;
18+
19+
// Team vesting
20+
uint64 public constant TEAM_CLIFF = 365 days;
21+
uint64 public constant TEAM_VESTING_DURATION = 730 days;
22+
23+
// Investor step vesting
24+
uint64 public constant INVESTOR_STEP = 180 days; // 6 months
25+
uint8 public constant INVESTOR_STEPS = 4; // 24 months total
26+
27+
// =========================
28+
// ======== STORAGE ========
29+
// =========================
30+
31+
VestingWallet[] public teamVestings;
32+
InvestorStepVesting public investorVesting;
33+
34+
// =========================
35+
// ========= EVENTS ========
36+
// =========================
37+
38+
event TeamVestingCreated(address indexed beneficiary, address vestingWallet, uint256 amount);
39+
event InvestorVestingCreated(address indexed investor, address vestingWallet, uint256 amount);
40+
event PublicDistributed(address indexed publicWallet, uint256 amount);
41+
42+
// =========================
43+
// ======== CONSTRUCTOR ====
44+
// =========================
45+
46+
constructor(address publicWallet, address[] memory teamWallets, address investorWallet, uint64 startTimestamp)
47+
ERC20("Paradaise Supply Chain", "PSC")
48+
Ownable(msg.sender)
49+
{
50+
require(publicWallet != address(0), "Invalid public wallet");
51+
require(investorWallet != address(0), "Invalid investor wallet");
52+
require(teamWallets.length == 15, "Team wallets must be 15");
53+
require(startTimestamp >= block.timestamp, "Start must be future");
54+
55+
for (uint256 i = 0; i < teamWallets.length; i++) {
56+
require(teamWallets[i] != address(0), "Invalid team wallet");
57+
}
58+
59+
_mint(address(this), TOTAL_SUPPLY);
60+
61+
// ===== Public =====
62+
uint256 publicAmount = (TOTAL_SUPPLY * PUBLIC_PERCENT) / 100;
63+
_transfer(address(this), publicWallet, publicAmount);
64+
emit PublicDistributed(publicWallet, publicAmount);
65+
66+
// ===== Team Vesting =====
67+
uint256 teamTotalAmount = (TOTAL_SUPPLY * TEAM_PERCENT) / 100;
68+
uint256 perTeamMember = teamTotalAmount / teamWallets.length;
69+
70+
for (uint256 i = 0; i < teamWallets.length; i++) {
71+
VestingWallet vesting =
72+
new VestingWallet(teamWallets[i], startTimestamp + TEAM_CLIFF, TEAM_VESTING_DURATION);
73+
74+
_transfer(address(this), address(vesting), perTeamMember);
75+
teamVestings.push(vesting);
76+
77+
emit TeamVestingCreated(teamWallets[i], address(vesting), perTeamMember);
78+
}
79+
80+
// ===== Investor Step Vesting =====
81+
uint256 investorAmount = TOTAL_SUPPLY - publicAmount - teamTotalAmount;
82+
83+
investorVesting = new InvestorStepVesting(investorWallet, startTimestamp, INVESTOR_STEP, INVESTOR_STEPS);
84+
85+
_transfer(address(this), address(investorVesting), investorAmount);
86+
87+
emit InvestorVestingCreated(investorWallet, address(investorVesting), investorAmount);
88+
}
89+
90+
// =========================
91+
// ========= VIEWS =========
92+
// =========================
93+
94+
function teamVestingsCount() external view returns (uint256) {
95+
return teamVestings.length;
96+
}
97+
98+
function getTeamVestings() external view returns (VestingWallet[] memory) {
99+
return teamVestings;
100+
}
101+
}
102+
103+
/// @title Investor Step Vesting (6-month unlocks)
104+
contract InvestorStepVesting is Ownable {
105+
address public immutable beneficiary;
106+
uint64 public immutable start;
107+
uint64 public immutable stepDuration;
108+
uint8 public immutable totalSteps;
109+
110+
uint256 public released;
111+
112+
constructor(address _beneficiary, uint64 _start, uint64 _stepDuration, uint8 _totalSteps) Ownable(msg.sender) {
113+
require(_beneficiary != address(0), "Invalid beneficiary");
114+
require(_totalSteps > 0, "Invalid steps");
115+
116+
beneficiary = _beneficiary;
117+
start = _start;
118+
stepDuration = _stepDuration;
119+
totalSteps = _totalSteps;
120+
}
121+
122+
receive() external payable {}
123+
124+
function releasable(address token) public view returns (uint256) {
125+
uint256 vested = vestedAmount(token, uint64(block.timestamp));
126+
return vested - released;
127+
}
128+
129+
function release(address token) external {
130+
uint256 amount = releasable(token);
131+
require(amount > 0, "Nothing to release");
132+
133+
released += amount;
134+
IERC20(token).transfer(beneficiary, amount);
135+
}
136+
137+
function vestedAmount(address token, uint64 timestamp) public view returns (uint256) {
138+
uint256 totalAllocation = IERC20(token).balanceOf(address(this)) + released;
139+
140+
if (timestamp < start) {
141+
return 0;
142+
}
143+
144+
uint256 elapsedSteps = (timestamp - start) / stepDuration + 1;
145+
146+
if (elapsedSteps >= totalSteps) {
147+
return totalAllocation;
148+
}
149+
150+
return (totalAllocation * elapsedSteps) / totalSteps;
151+
}
152+
}

0 commit comments

Comments
 (0)