Skip to content
Draft
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
33 changes: 33 additions & 0 deletions .agent/rules/solidity_zksync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Solidity & ZkSync Development Standards

## Toolchain & Environment
- **Primary Tool**: `forge` (ZkSync fork). Use for compilation, testing, and generic scripting.
- **Secondary Tool**: `hardhat`. Use only when `forge` encounters compatibility issues (e.g., complex deployments, specific plugin needs).
- **Network Target**: ZkSync Era (Layer 2).
- **Solidity Version**: `^0.8.20` (or `0.8.24` if strictly supported by the zk-compiler).

## Modern Solidity Best Practices
- **Safety First**:
- **Checks-Effects-Interactions (CEI)** pattern must be strictly followed.
- When a contract requires an owner (e.g., admin-configurable parameters), prefer `Ownable2Step` over `Ownable`. Do **not** add ownership to contracts that don't need it — many contracts are fully permissionless by design.
- Prefer `ReentrancyGuard` for external calls where appropriate.
- **Gas & Efficiency**:
- Use **Custom Errors** (`error MyError();`) instead of `require` strings.
- Use `mapping` over arrays for membership checks where possible.
- Minimize on-chain storage; use events for off-chain indexing.

## Testing Standards
- **Framework**: Foundry (Forge).
- **Methodology**:
- **Unit Tests**: Comprehensive coverage for all functions.
- **Fuzz Testing**: Required for arithmetic and purely functional logic.
- **Invariant Testing**: Define invariants for stateful system properties.
- **Naming Convention**:
- `test_Description`
- `testFuzz_Description`
- `test_RevertIf_Condition`

## ZkSync Specifics
- **System Contracts**: Be aware of ZkSync system contracts (e.g., `ContractDeployer`, `L2EthToken`) when interacting with low-level features.
- **Gas Model**: Account for ZkSync's different gas metering if performing low-level optimization.
- **Compiler Differences**: Be mindful of differences between `solc` and `zksolc` (e.g., `create2` address derivation).
11 changes: 10 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
"Frontends",
"testuser",
"testhandle",
"douglasacost"
"douglasacost",
"IBEACON",
"AABBCCDD",
"SSTORE",
"Permissionless",
"Reentrancy",
"SFID",
"EXTCODECOPY",
"solady",
"SLOAD"
]
}
33 changes: 33 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Solidity & ZkSync Development Standards

## Toolchain & Environment
- **Primary Tool**: `forge` (ZkSync fork). Use for compilation, testing, and generic scripting.
- **Secondary Tool**: `hardhat`. Use only when `forge` encounters compatibility issues (e.g., complex deployments, specific plugin needs).
- **Network Target**: ZkSync Era (Layer 2).
- **Solidity Version**: `^0.8.20` (or `0.8.24` if strictly supported by the zk-compiler).

## Modern Solidity Best Practices
- **Safety First**:
- **Checks-Effects-Interactions (CEI)** pattern must be strictly followed.
- Use `Ownable2Step` over `Ownable` for privileged access.
- Prefer `ReentrancyGuard` for external calls where appropriate.
- **Gas & Efficiency**:
- Use **Custom Errors** (`error MyError();`) instead of `require` strings.
- Use `mapping` over arrays for membership checks where possible.
- Minimize on-chain storage; use events for off-chain indexing.

## Testing Standards
- **Framework**: Foundry (Forge).
- **Methodology**:
- **Unit Tests**: Comprehensive coverage for all functions.
- **Fuzz Testing**: Required for arithmetic and purely functional logic.
- **Invariant Testing**: Define invariants for stateful system properties.
- **Naming Convention**:
- `test_Description`
- `testFuzz_Description`
- `test_RevertIf_Condition`

## ZkSync Specifics
- **System Contracts**: Be aware of ZkSync system contracts (e.g., `ContractDeployer`, `L2EthToken`) when interacting with low-level features.
- **Gas Model**: Account for ZkSync's different gas metering if performing low-level optimization.
- **Compiler Differences**: Be mindful of differences between `solc` and `zksolc` (e.g., `create2` address derivation).
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/era-contracts"]
path = lib/era-contracts
url = https://github.com/matter-labs/era-contracts
[submodule "lib/solady"]
path = lib/solady
url = https://github.com/vectorized/solady
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@
"editor.formatOnSave": true,
"[solidity]": {
"editor.defaultFormatter": "JuanBlanco.solidity"
},
"chat.tools.terminal.autoApprove": {
"forge": true
}
}
20 changes: 20 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lib/zksync-storage-proofs": {
"rev": "4b20401ce44c1ec966a29d893694f65db885304b"
},
"lib/openzeppelin-contracts": {
"rev": "e4f70216d759d8e6a64144a9e1f7bbeed78e7079"
},
"lib/solady": {
"tag": {
"name": "v0.1.26",
"rev": "acd959aa4bd04720d640bf4e6a5c71037510cc4b"
}
},
"lib/forge-std": {
"rev": "1eea5bae12ae557d589f9f0f0edae2faa47cb262"
},
"lib/era-contracts": {
"rev": "84d5e3716f645909e8144c7d50af9dd6dd9ded62"
}
}
1 change: 1 addition & 0 deletions lib/solady
Submodule solady added at acd959
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@openzeppelin=lib/openzeppelin-contracts/
@openzeppelin=lib/openzeppelin-contracts/
solady/=lib/solady/src/
145 changes: 145 additions & 0 deletions src/swarms/FleetIdentity.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
* @title FleetIdentity
* @notice ERC-721 with ERC721Enumerable representing ownership of a BLE fleet,
* secured by an ERC-20 bond that is locked on mint and refunded on burn.
* @dev TokenID = uint256(uint128(uuid)), guaranteeing one owner per Proximity UUID.
* Bond amounts are increase-only and refunded in full when the NFT is burned.
*/
contract FleetIdentity is ERC721Enumerable, ReentrancyGuard {
using SafeERC20 for IERC20;

// ──────────────────────────────────────────────
// Errors
// ──────────────────────────────────────────────
error InvalidUUID();
error NotTokenOwner();
error ZeroBondAmount();
error BondBelowMinimum();

// ──────────────────────────────────────────────
// State
// ──────────────────────────────────────────────

/// @notice The ERC-20 token used for bonds (immutable, e.g. NODL).
IERC20 public immutable BOND_TOKEN;

/// @notice Minimum bond required to register a fleet (set once at deploy).
uint256 public immutable MIN_BOND;

/// @notice TokenID -> cumulative bond deposited.
mapping(uint256 => uint256) public bonds;

// ──────────────────────────────────────────────
// Events
// ──────────────────────────────────────────────

event FleetRegistered(address indexed owner, bytes16 indexed uuid, uint256 indexed tokenId, uint256 bondAmount);
event BondIncreased(uint256 indexed tokenId, address indexed depositor, uint256 amount, uint256 newTotal);
event FleetBurned(address indexed owner, uint256 indexed tokenId, uint256 bondRefund);

// ──────────────────────────────────────────────
// Constructor
// ──────────────────────────────────────────────

/// @param _bondToken Address of the ERC-20 token used for bonds.
/// @param _minBond Minimum bond required to register a fleet.
constructor(address _bondToken, uint256 _minBond) ERC721("Swarm Fleet Identity", "SFID") {
BOND_TOKEN = IERC20(_bondToken);
MIN_BOND = _minBond;
}

// ──────────────────────────────────────────────
// Core
// ──────────────────────────────────────────────

/// @notice Mints a new fleet NFT for the given Proximity UUID and locks a bond.
/// @param uuid The 16-byte Proximity UUID.
/// @param bondAmount Amount of BOND_TOKEN to lock (must be >= minBond).
/// @return tokenId The deterministic token ID derived from `uuid`.
function registerFleet(bytes16 uuid, uint256 bondAmount) external nonReentrant returns (uint256 tokenId) {
if (uuid == bytes16(0)) revert InvalidUUID();
if (bondAmount < MIN_BOND) revert BondBelowMinimum();

tokenId = uint256(uint128(uuid));

// CEI: effects before external call
bonds[tokenId] = bondAmount;
_mint(msg.sender, tokenId);

// Interaction: pull bond from caller
BOND_TOKEN.safeTransferFrom(msg.sender, address(this), bondAmount);

emit FleetRegistered(msg.sender, uuid, tokenId, bondAmount);
}

/// @notice Increases the bond for an existing fleet. Anyone can top-up.
/// @param tokenId The fleet token ID.
/// @param amount Additional BOND_TOKEN to lock.
function increaseBond(uint256 tokenId, uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroBondAmount();

// ownerOf reverts for nonexistent tokens — acts as existence check
ownerOf(tokenId);

// CEI: effects before external call
bonds[tokenId] += amount;

// Interaction
BOND_TOKEN.safeTransferFrom(msg.sender, address(this), amount);

emit BondIncreased(tokenId, msg.sender, amount, bonds[tokenId]);
}

/// @notice Burns the fleet NFT and refunds the entire bond to the token owner.
/// @param tokenId The fleet token ID to burn.
function burn(uint256 tokenId) external nonReentrant {
address tokenOwner = ownerOf(tokenId);
if (tokenOwner != msg.sender) revert NotTokenOwner();

// CEI: effects before external call
uint256 refund = bonds[tokenId];
delete bonds[tokenId];
_burn(tokenId);

// Interaction: refund bond
if (refund > 0) {
BOND_TOKEN.safeTransfer(tokenOwner, refund);
}

emit FleetBurned(tokenOwner, tokenId, refund);
}

// ──────────────────────────────────────────────
// View helpers
// ──────────────────────────────────────────────

/// @notice Convenience: returns the UUID for a given token ID.
function tokenUUID(uint256 tokenId) external pure returns (bytes16) {
return bytes16(uint128(tokenId));
}

// ──────────────────────────────────────────────
// Overrides required by ERC721Enumerable
// ──────────────────────────────────────────────

function _update(address to, uint256 tokenId, address auth) internal override(ERC721Enumerable) returns (address) {
return super._update(to, tokenId, auth);
}

function _increaseBalance(address account, uint128 value) internal override(ERC721Enumerable) {
super._increaseBalance(account, value);
}

function supportsInterface(bytes4 interfaceId) public view override(ERC721Enumerable) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
53 changes: 53 additions & 0 deletions src/swarms/ServiceProvider.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";

/**
* @title ServiceProvider
* @notice Permissionless ERC-721 representing ownership of a service endpoint URL.
* @dev TokenID = keccak256(url), guaranteeing one owner per URL.
*/
contract ServiceProvider is ERC721 {
error EmptyURL();
error NotTokenOwner();

// Maps TokenID -> Provider URL
mapping(uint256 => string) public providerUrls;

event ProviderRegistered(address indexed owner, string url, uint256 indexed tokenId);
event ProviderBurned(address indexed owner, uint256 indexed tokenId);

constructor() ERC721("Swarm Service Provider", "SSV") {}

/// @notice Mints a new provider NFT for the given URL.
/// @param url The backend service URL (must be unique).
/// @return tokenId The deterministic token ID derived from `url`.
function registerProvider(string calldata url) external returns (uint256 tokenId) {
if (bytes(url).length == 0) {
revert EmptyURL();
}

tokenId = uint256(keccak256(bytes(url)));

providerUrls[tokenId] = url;

_mint(msg.sender, tokenId);

emit ProviderRegistered(msg.sender, url, tokenId);
}

/// @notice Burns the provider NFT. Caller must be the token owner.
/// @param tokenId The provider token ID to burn.
function burn(uint256 tokenId) external {
if (ownerOf(tokenId) != msg.sender) {
revert NotTokenOwner();
}

delete providerUrls[tokenId];

_burn(tokenId);

emit ProviderBurned(msg.sender, tokenId);
}
}
Loading
Loading