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
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# --- Hedera network -------------------------------------------------------
HEDERA_NETWORK=testnet
# --- Hedera operator ------------------------------------------------------
# Operator = deployer + token treasury/keys for the MVP. Funded testnet account on chain 296.
# Raw-hex ECDSA key (EVM-style); hardhat.config signs the testnet network with it.
OPERATOR_ID=0.0.xxxxxx
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
push:
pull_request:

jobs:
contracts:
name: contracts (compile · typecheck · test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run compile
- run: pnpm run typecheck
- run: pnpm test

web:
name: web (build)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: web/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
working-directory: web
- run: pnpm build
working-directory: web
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ artifacts/
cache/
typechain-types/
*.mov

# Local prep & internal notes (not part of the public repo)
/notes/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Wafer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
314 changes: 227 additions & 87 deletions README.md

Large diffs are not rendered by default.

31 changes: 12 additions & 19 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ contract** on Hedera EVM (HSCS) creating/holding **HTS tokens** via `@hiero-ledg
Scripts + frontend: TypeScript (viem + React). Settlement: **native HBAR**. Target track:
**Hedera — Tokenization**.

> This spec is the build blueprint. **If it is implemented as written, the app ships.** The current
> deployed contract (`0xc452D2…cd0A`), `scripts/`, `test/`, and `web/` reference the OLD shape and
> must be re-generated per §12 (Migration). Nothing is mocked except the **DePIN reward cashflow**
> (§9), modeled on-chain by `MockRewardSource`.
> This spec is the build blueprint, implemented as written and **live on Hedera Testnet**
> (Sourcify-verified; canonical addresses in [`deployments/testnet.json`](deployments/testnet.json)).
> Nothing is mocked except the **DePIN reward cashflow** (§9), modeled on-chain by `MockRewardSource`.

---

Expand Down Expand Up @@ -432,21 +431,15 @@ Gas override on HTS-touching calls (`gas ~1M`, `maxFeePerGas = liveBaseFee×5 +
IHRC719 + SaucerSwap router), `format.js` (8dp/tinybar + weibar boundary), `mirror.js`,
`errors.js`. The app reads live from the deployed `VITE_VAULT_ADDRESS` (no mock mode at ship).

## 12. Migration from the current contract

The redesign **renames/splits** storage and changes signatures — enumerate so nothing breaks:
- `Pool.totalAssets` → **`idleTinybar` + `receivableTinybar`** (totalAssets becomes a view).
- `Claim.principalTinybar` → **`advance/expected/carry/settled/startTime/termSeconds`** (+ device
fields). New `Deal` struct + proposal flow. New `RedemptionRequest` queue.
- `financeClaim(poolId, operator, principal, meta)` → **`proposeDeal` + `approveDeal` +
`financeClaim(dealId)`** (term/expected now come from the Deal).
- `settleRewards` → amortized + gated + capped; `markDefault` writes down **carry, not principal**.
- New events (D12). New keys-exempt fee config. Ownable2Step + timelock + operator whitelist.
- **Re-generate:** `contracts/WaferVault.sol`, `contracts/MockRewardSource.sol`,
`contracts/MockDeviceNFT.sol`, `scripts/deploy.ts` (seed dead shares, register operator),
`scripts/smoke.ts` (propose→approve→finance→drip→repaid/burn; default run), `test/*` (see §13),
`web/lib/abi.js` + `config.js`, `deployments/testnet.json`. **Redeploy** — the old vault
`0xc452D2…cd0A` and its hbar-units tests (which certify the bug) are deprecated.
## 12. Design history (shipped)

The shipped contract is the amortized-cost redesign described above. For the record, the key changes
from the earliest prototype were: `Pool.totalAssets` split into derived **`idleTinybar` +
`receivableTinybar`** (killing the double-count bug); a single `financeClaim(poolId, operator,
principal)` replaced by the **`proposeDeal` → `approveDeal` → `financeClaim(dealId)`** workflow;
`settleRewards` made amortized + gated + capped and `markDefault` writing down **carry, not
principal**; plus Ownable2Step, a timelock, the operator allowlist, and the redemption queue. All of
this is live and verified — see [`deployments/testnet.json`](deployments/testnet.json).

## 13. Testing (required before "ship")

Expand Down
12 changes: 3 additions & 9 deletions contracts/MockRewardSource.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ interface IWaferVaultSettle {
*/
contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService {
uint256 internal constant SUCCESS = uint256(int256(HederaResponseCodes.SUCCESS)); // 22
// Gas budget for an HSS-scheduled scheduledDrip: it must cover the settle (~125k, plus the HTS
// burn/return on the repay interval) AND the reschedule (one scheduleCall ≈ 1.3M gas), so this is
// generously sized — a too-small limit reverts the whole scheduled tx (no settle, chain stops).
// Gas budget for the HSS-scheduled scheduledDrip (a single maturity settle): it must cover the
// settle plus the HTS claim-NFT burn + device-NFT return on the repay interval, so it is
// generously sized — a too-small limit would revert the whole scheduled tx and skip the settle.
uint256 internal constant DRIP_GAS = 6_000_000;

IWaferVaultSettle public immutable vault;
Expand All @@ -43,7 +43,6 @@ contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService {
}

Schedule[] public schedules;
mapping(uint256 => bool) public selfScheduling; // scheduleId => on-chain HIP-1215 self-drip armed

event Funded(uint256 indexed scheduleId, uint32 poolId, uint256 claimId, uint64 totalReward, uint64 startTime, uint64 termSeconds, uint32 dripCount);
event Dripped(uint256 indexed scheduleId, uint32 intervalsReleased, uint64 amount);
Expand Down Expand Up @@ -128,10 +127,6 @@ contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService {
// HIP-1215 self-scheduling drip (no off-chain keeper / JS loop, SPEC §9)
// -------------------------------------------------------------------------

/// @notice Arm on-chain self-scheduling for a funded schedule: each interval's drip is scheduled
/// on-chain via the Hedera Schedule Service (HSS, 0x16b) and reschedules the next, until
/// the term completes — replacing the off-chain keeper / JS poll loop. The contract is the
/// schedule payer, so it must hold the prefunded reward HBAR (it does, from fund()).
/// @notice Arm a keeper-free reward settlement via HSS: schedule ONE scheduledDrip at maturity
/// (startTime + termSeconds) that releases the full reward in a single Hedera-scheduled
/// transaction — no off-chain keeper, no JS loop.
Expand All @@ -145,7 +140,6 @@ contract MockRewardSource is Ownable, ReentrancyGuard, HederaScheduleService {
require(scheduleId < schedules.length, "NO_SCHEDULE");
Schedule storage s = schedules[scheduleId];
require(!s.defaulted, "DEFAULTED");
selfScheduling[scheduleId] = true;

uint64 at = s.startTime + s.termSeconds; // maturity: by now every interval is due
if (at <= uint64(block.timestamp)) at = uint64(block.timestamp) + 3;
Expand Down
7 changes: 3 additions & 4 deletions contracts/WaferVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import "@hiero-ledger/hiero-contracts/token-service/HederaTokenService.sol";
import "@hiero-ledger/hiero-contracts/token-service/IHederaTokenService.sol";
import "@hiero-ledger/hiero-contracts/token-service/KeyHelper.sol";
import "@hiero-ledger/hiero-contracts/token-service/ExpiryHelper.sol";
import "@hiero-ledger/hiero-contracts/token-service/FeeHelper.sol";
import "@hiero-ledger/hiero-contracts/common/HederaResponseCodes.sol";
import "@hiero-ledger/hiero-contracts/schedule-service/HederaScheduleService.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
Expand Down Expand Up @@ -57,7 +56,7 @@ interface IShareApprove {
* on HTS business errors (KYC not granted, frozen, ...). We therefore check responseCode == 22
* (SUCCESS) on EVERY HTS call and revert otherwise.
*/
contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, ExpiryHelper, FeeHelper, Ownable2Step, ReentrancyGuard {
contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, ExpiryHelper, Ownable2Step, ReentrancyGuard {
// --- constants -----------------------------------------------------------
uint256 internal constant ONE = 1e8; // 1.0 in tinybar / share micro-units (8 dp)
int32 internal constant SHARE_DECIMALS = 8;
Expand Down Expand Up @@ -86,7 +85,7 @@ contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, Exp
// Gas budget for the HIP-1215 scheduled `releaseAdvance` execution (one state write + one payout).
uint256 internal constant RELEASE_ADVANCE_GAS = 400_000;

// HIP-1215 "locked virement": if advanceLockSeconds > 0, financeClaim LOCKS the advance in the
// HIP-1215 "locked transfer": if advanceLockSeconds > 0, financeClaim LOCKS the advance in the
// vault and schedules a Hedera Schedule Service (HSS, 0x16b) call that auto-releases it to the
// operator after the window — no keeper. 0 = pay the advance immediately at finance (default).
uint64 public advanceLockSeconds = 0;
Expand Down Expand Up @@ -647,7 +646,7 @@ contract WaferVault is HederaTokenService, HederaScheduleService, KeyHelper, Exp
(bool ok, ) = payable(d.operator).call{value: d.advanceTinybar}("");
require(ok, "HBAR_ADVANCE_FAIL");
} else {
// HIP-1215 "locked virement": the advance stays in the vault (earmarked in
// HIP-1215 "locked transfer": the advance stays in the vault (earmarked in
// pendingAdvanceTinybar) and an HSS-scheduled call auto-releases it at unlock time.
uint64 unlockAt = uint64(block.timestamp) + advanceLockSeconds;
advanceUnlockTime[claimId] = unlockAt;
Expand Down
46 changes: 23 additions & 23 deletions deployments/testnet.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
{
"network": "testnet",
"chainId": 296,
"createdAt": "2026-06-14T11:00:00.923Z",
"updatedAt": "2026-06-14T13:11:06.540Z",
"createdAt": "2026-06-16T07:07:39.493Z",
"updatedAt": "2026-06-16T07:07:39.493Z",
"settlementAsset": "HBAR",
"operator": "0xf6fAc89C3a2bAA468c78d3A638cA2F44F5fdBDbF",
"vaultAddress": "0x4B821d6bC76203C3C21131849C40d04C84bb75d5",
"vaultId": "0.0.9231166",
"vaultAddress": "0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311",
"vaultId": "0.0.9250244",
"pool": {
"id": 0,
"name": "Wafer GPU-A",
"symbol": "wGPUA",
"category": "GPU",
"class": "A",
"shareTokenEvm": "0x00000000000000000000000000000000008cDB41",
"shareTokenId": "0.0.9231169",
"claimNftEvm": "0x00000000000000000000000000000000008CDB42",
"claimNftId": "0.0.9231170"
"shareTokenEvm": "0x00000000000000000000000000000000008D25c5",
"shareTokenId": "0.0.9250245",
"claimNftEvm": "0x00000000000000000000000000000000008D25c6",
"claimNftId": "0.0.9250246"
},
"mocks": {
"rewardSource": {
"evm": "0xe621ea918299E1ED5f69bB8c85D879F4E88aAa1f",
"id": "0.0.9232002"
"evm": "0x70059b045740600D735Ec69Fa37489d990209007",
"id": "0.0.9250248"
},
"deviceNft": {
"evm": "0x2618837d4087726cdF67C1B6ee26f39df75Ef6B3",
"id": "0.0.9231175",
"collectionEvm": "0x00000000000000000000000000000000008cDB48",
"collectionId": "0.0.9231176"
"evm": "0xd40e1b1A7119d40426494262F391C239adC0C149",
"id": "0.0.9250249",
"collectionEvm": "0x00000000000000000000000000000000008D25CA",
"collectionId": "0.0.9250250"
}
},
"secondary": {
Expand All @@ -36,14 +36,14 @@
"factory": "0x00000000000000000000000000000000000026e7"
},
"hashscan": {
"vault": "https://hashscan.io/testnet/contract/0x4B821d6bC76203C3C21131849C40d04C84bb75d5",
"shareToken": "https://hashscan.io/testnet/token/0.0.9231169",
"claimNft": "https://hashscan.io/testnet/token/0.0.9231170",
"rewardSource": "https://hashscan.io/testnet/contract/0xe621ea918299E1ED5f69bB8c85D879F4E88aAa1f",
"deviceNft": "https://hashscan.io/testnet/contract/0x2618837d4087726cdF67C1B6ee26f39df75Ef6B3",
"deviceCollection": "https://hashscan.io/testnet/token/0.0.9231176",
"deployTx": "https://hashscan.io/testnet/transaction/0xf1026dc97e200697e6c991463c79e2a2487a5b45fd8a212a19d94c6484aa7074",
"createPoolTx": "https://hashscan.io/testnet/transaction/0xf0c31f601c5d739d846e96aa2bb32876a200b00afe1f8ac6e86d9c7b768456cd"
"vault": "https://hashscan.io/testnet/contract/0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311",
"shareToken": "https://hashscan.io/testnet/token/0.0.9250245",
"claimNft": "https://hashscan.io/testnet/token/0.0.9250246",
"rewardSource": "https://hashscan.io/testnet/contract/0x70059b045740600D735Ec69Fa37489d990209007",
"deviceNft": "https://hashscan.io/testnet/contract/0xd40e1b1A7119d40426494262F391C239adC0C149",
"deviceCollection": "https://hashscan.io/testnet/token/0.0.9250250",
"deployTx": "https://hashscan.io/testnet/transaction/0xceabb69cf6256101ab0e78f58feeb2bdb0e6a45a9e2fb4713c7b122343c3a8b1",
"createPoolTx": "https://hashscan.io/testnet/transaction/0xcf2173af23e2e83eb3bd49c86fdd1dee0f521241720ef29fe0d2378c9cda5e19"
},
"sourcify": "https://repo.sourcify.dev/contracts/full_match/296/0x4B821d6bC76203C3C21131849C40d04C84bb75d5/"
"sourcify": "https://repo.sourcify.dev/contracts/full_match/296/0x8Fb4439f76ea7eAa6DcE88751A20981a796fb311/"
}
Loading
Loading