Token and Decentralized Municipal Property Management System
Del Norte Token and Municipal System Version 1.7
Design document – not final ken@usfintechllc.com Last updated April 9th, 2025
History
V1.25 01/21/2025 V1.3 03/07/2025 V1.5 03/15/2025 V1.6 3/21/25
Provided by US Fintech, LLC for Del Norte US Fintech is CTO to Del Norte under the technical leadership of Ken Silverman |
Authors: Ken Silverman. GPT 4.5 ongoing audit provided in final chapter.
Table of Contents
Design Overview (Page 2)
Token Contract System (Page 6 -15)
Elastic Treasury Model (Page 7)
- 2.1 Token Definition (Page 39 - code complete - whitelist - and remote registrar)
- 2.2 Sale and Swap Contract (code complete - confirms whitelist creates schedule)
- 2.3 Vesting Contract (page 54 - code complete - ReleaseManager)
- 2.4 Liquidity Pool Contract (in development)
- 2.5 Rewards Contract with Account Tranches (Earn before claim)
- 2.6 Membership contract map(Member’s publicAddress : membershipLevel ( = tokens forever locked in MembershipContract)
Municipal Property Management and Registration System (MPMRS) (Page 50)
MPMRS Smart Contract (MPMRS_SC) and ERC-721 Management (Page 57)
Owner Wallet Website Explorer Services and MRS_SC Supporting Methods
Redesign of MPMRS_SC with IPFS Included (Pages 78-91)
Locked Membership/Loyalty Token Addresses for Services (page 92 To Be Written)
Login and Transaction Authorization Eth-based Device Key Flow (page 100)
AI Audit of existing code
1. Design Components
1. Token System
The token system facilitates the distribution of service fees to token holders and the initial capital raise. It comprises the following:
- Five principal smart contracts: Gated Token, Sale, Vesting (scheduleReleaseManager) , liquidity pool and Earned Rewards contract (Rewards earned at time of claiming via Validation of documents for selected owners).
- Webhooks: For external integrations.
- Registration agent: To manage KYC and address whitelisting.
- User wallet connectivity: To interact with the system via decentralized wallets.
2. Municipal Property Management and Registration System (MPMRS)
The Municipal Registration System integrates centralized and decentralized components to mint properties as ERC-721 compliant tokens and manage metadata efficiently. Its core elements include:
- Centralized Software: Scans and stores property data in cloud infrastructure, such as Google Cloud.
- MPMRS Smart Contract (MRS_SC): Bridges tokenIds (NFTs) to propertyIds on-chain, supporting ownership and metadata updates.
- Municipal Wallet: Manages property tokens, enabling minting, updates, and ownership tracking.
Key Processes in the MPMRS – Metadata management
- Metadata Creation:
- Metadata uses a flat structure, storing key-value pairs for documents, images, and property records as CIDs.
- Historical data is preserved, and the most recent records remain actively relevant.
- IPFS Integration:
- Each update generates a new CID for the changed data, stored in IPFS.
- Metadata structured as flat sectioned CID structure with multiplied CIDS. Optionally a CID (hash of all CIDs) can summarize the metadata, allowing only a single cid stored in the NFT, instead of multiple mappings stored in the NFT as presented here. But this would require IPFS access for all activity that could otherwise be somewhat understood through a blockchain explorer to monitor payments, for example associated with titles and no dependence on IPFS for basic queries.
- Smart Contract Interaction:
- The MRS_SC mints property NFTs, links them to their propertyIds and stores metadata CIDs.
- For existing propertyIds, it supports updates, registrant changes, and shared roles for modifying specific fields.
3. The Property Owner Wallet
The property owner wallet allows end-users to:
- View the status of their properties.
- Pay taxes or fees associated with their properties.
- Interact with Web-3-based storage and write metadata fingerprints (CIDs) to the smart contract, ensuring immutability and transparency.
4. IPFS (Web-3 Metadata Storage)
IPFS serves as the decentralized storage layer for metadata, featuring:
- Sub-isomorphic shards with hysteresis: To maintain data consistency and resilience.
- Local nodes: Run as part of each MRS client, ensuring redundant metadata handling.
- Web-3 cluster of independent full nodes: full iso., guarantees retrieval if network down.
The four components are linked and loosely described according to Figure 1. (Next Page)

Figure 1
2 The Token System - all SCs extend ElasticTreasuryHub or ElasticTreasurySpoke which extend BaseTreasury
Set of six Solidity contracts—Token, Sale, Vesting, LP, Membership and Rewards.
Token contract
- Standard ERC20 token with a fixed cap of 850,000,000 tokens, 8-decimal places.
- Accepts a multi-sig initialAdmin and a masterHub address in constructor.
- WhitelistedUsers map [Key:pubAddress Value:RegistrationNotes] ( replaces registrars array/ signing verification as primary registrtn. method.
BaseTreasury MasterHub contract controls access:
- Official Smart Contract Addresses (All SC Addresses Exempt from 1404-gating requirement. These are also treasury addresses. Del Norte Smart contract addresses that have permission to transfer without gating, (sales, vesting, LP, Membership etc…) No additions to this array except by ElasticTreasuryHub via BaseTreasury which auto-adds the address if not already here.
- TreasuryAdmins (Addresses of Del Norte directors that have permission to
- Call any elasticTreasuryTransfer method on any ElasticTreasuryHub that has this SC address as its masterHub
TokenAdmins - redundant, all TreasuryAdmins are TokenAdmins.
Auxiliary Structures
- registrarPubKeys [ ] Secondary to whitelisted users (registrars from which any user who is registered gets a registration key with the message ({"pubAddressUser" : xxxxxx}). Approval verified in the method isRestricted() : regKey passed into our transferWithRegKey(regKey,registrarPubKey) {
Is registrarPublicKey in registrarPubKeys? Yes
Is KECCACK(method.sender,registrarPubKey) and ERCrevover() equal to passed regKey? Yes, Allow transfer!
Elastic Treasury Model
Controller (pointed to) ---------------------
▲ ▲ │ │ │ │ abstract contract ElasticTrsHub abstract contract ElasticTrsSpoke (address controller) (address controller) ----------------------- ----------------------- ▲ ▲ └──────┬─────────┘ ▼ contract Token contract SalesContract is ElasticTreasHub is ETreasHub, ElasticTreasSpoke --------------------- |
|
The token contract is a MasterHub. It’s admins can transfer ETH or any ERC-20 token from any ElasticTreasuryHub such as itself to any ElasticTreasurySpoke contract using a TreasuryTransfer method defined in ElasticTreasuryHub. The spoke accepts a fixed hubAddress and MasterHub address at deployment. The hub accepts only a masterHub and initialAdmin at deployment. The Token Contract acts as the central token treasury, and therefore implements the abstract class called ElasticTreasuryHub. The Hub must deploy with the address of any other Hub (including self) called MasterHubAddress from which the isAdmin() function is called for which to verify if msg.sender is authorized to call treasuryTransfer() method on the Hub (TokenContact and SalesContract are both hubs).
HUB and SPOKE support Diamond inheritance, and are therefore virtual. Later we will see how the SalesContract will implement both Spoke (to receive DTV tokens) and Hub (to send USDC tokens to the USDC liquidity pool contract and Vesting contract for payouts to independent contractors with USDC schedules.
1. Overview
The Elastic Treasury model decentralizes treasury management by empowering each smart contract (spoke) within the ecosystem to act as its own treasury. The central hub (the Token Contract) maintains a registry of each contract's treasury allocation, allowing secure, controlled transfers and reclaim mechanisms.
Benefits
- Decentralization: Each smart contract manages its own allocated funds.
- Security: Multi-sig executives control all treasury interactions.
- Transparency: Precise tracking of allocations, transfers, and reclaimed amounts.
2. Elastic Treasury Hub-and-Spoke Model
+-------------------+ | Token Contract | | (Hub) | +---------+---------+ | -------------------------------------------- | | | +----------------------+ +---------------------+ +----------------------+ | Sales Contract | | Vesting Contract | | Liquidity Contract | | treasuryReceive() | | treasuryReceive() | | treasuryReceive() | | treasuryReclaim() | | treasuryReclaim() | | treasuryReclaim() | +----------------------+ +---------------------+ +----------------------+
Each spoke maintains internal records tracking amounts received from and reclaimed by the Token Contract.
--- ## Data Structures ### ElasticTreasuryHub (such as Token Contract and Sales Contract) // Represents treasury tracking state for each token type (ERC20 or ETH) struct SingleCoinTreasuryState { string label; // Label for the single token treasury state uint256 totalTransferred; // Total transferred out to spoke uint256 totalReclaimed; // Total reclaimed back from spoke uint256 totalTimesTransferred; // Total number of successful transfers uint256 totalTimesReclaimed; // Total successful reclaim actions uint256[] failedReclaimAttemptAmounts; // Attempted amounts that exceeded totalTransferred uint256[] bouncedReclaimAmounts; // Reclaim attempts exceeding reclaimable balance } // Treasury structure for all coins (ERC20 + ETH) managed by a specific Spoke |
struct AllCoinTreasury { mapping(address => SingleCoinTreasuryState) tokenTreasury; // ERC20 tokens mapped by token address SingleCoinTreasuryState ethTreasury; // Dedicated ETH treasury state string label; // Label unique at the Spoke level }
// Main mapping: Spoke Smart Contract address => AllCoinTreasury mapping(address => AllCoinTreasury) public treasury; |
|
Subcontract (Spokes):
struct SpokeTreasuryEntry { uint256 totalReceived; // Tokens received from the Hub Contract uint256 totalReclaimed; // Tokens reclaimed by the Hub Contract } mapping(address => SpokeTreasuryEntry) public spokeTreasury; address public hubTokenAddress; // Address of Token Contract (Hub) |
treasuryReclaimRequest exists on the Hub (Token Contract extends ElasticTreasuryHub) which calls treasuryReclaim on the Spoke (spoke smart contracts extend ElasticTreasurySpoke). Each method serves a different purpose:
- On the Hub (Token Contract / Sales Contract both extend ElasticTreasuryHub):
- treasuryReclaimRequest initiates the reclaim process.
- It calculates the reclaimable amount and ensures limits are respected.
- Calls the spoke contract's treasuryReclaim method to request tokens back.
- Updates the hub's internal records upon successful reclaim.
- On the Spoke (Smart Contracts):
- treasuryReclaim responds to the reclaim request initiated by the hub.
- Verifies the reclaim request came from the correct hub address.
- Transfers the requested tokens back to the hub.
- Updates the spoke’s internal record of reclaimed amounts.
This ensures secure and transparent reclaiming of funds between the hub and spokes.
Elastic Treasury Network
Treasury Diagram - classes Controller (deployed with an intialAdmin address. Pointed to by:
|--> abstract ElasticTreasuryHub | | | |--> TokenContract | |--> SalesContract (also a spoke) | |--> abstract ElasticTreasurySpoke | |--> SalesContract (also a hub) |--> VestingContract |--> LiquidityContract |
Where ElasticTreasuryHub has these specific structures:
ElasticTreasuryHub (inherits BaseTreasury) │ └─ treasury (mapping of spoke smart contract addresses) │ ├─ AllCoinTreasury [Label must be unique per spoke contract] │ ├── label: "Sales", "Vesting", etc. [checked for uniqueness at spoke-level] │ │ │ ├── ethTreasury (SingleCoinTreasuryState) │ │ ├── label: unrestricted (but immutable once set) │ ├── totalTransferred, totalReclaimed, etc. │ │ └── tokenTreasury (mapping tokenContract addresses) │ ├── SingleCoinTreasuryState │ │ ├── label: set once, checked on subsequent transfers │ │ ├── totalTransferred, totalReclaimed, etc. │ │ │ └── SingleCoinTreasuryState (other token address) │ └─ Another SpokeContract... |
Smart Contract: Recovery
Recovery robustly manages accidental ETH and ERC-20 token transfers, incorporating admin fee management and secure administrative control. Contracts extending this are granted services to accurately track incoming ETH and token balances. Anyone can implement and call reversal functions to reverse their own payments after an admin (specifically a TreasuryAdmin of the controller) updates the reversible amount.
Clarifications on Treasury Management
The following clarifications explain proper separation and accurate state management between pre-deployed Controller and ElasticTreasury functions:
- HUB/SPOKE independently track direct treasury transfers. They remain isolated from Elastic Treasury functions (treasuryTransfer, treasuryReclaim), which have their dedicated state management but use the Controller’s modifier for security. (see Controller’s modifier onlyBTExecutives)
- Elastic Treasury transfers and reclaims are intentionally isolated from updating any other balance types like that found in Recovery.
- Historical accuracy for auditability necessitates separate tracking of amounts received and reversed (Recovery), distinct from the Elastic Treasury's internal states.
Controller now handles ALL ADMINS including the BTAdmin (TreasuryAdmin) which is unique in that only the BTAdmin is required to be in the MasterHub (see OnlyBTExecutives modifier which calls the masterHub hasBTRole). Diagram of permission structures is here:
Controller is Recovery
│
├── officialSCAddresses (mapping + array)
│ ├── isOfficialSCAddress(address) → bool
│ ├── getAllOfficialSCAddresses() → address[]
│ ├── addOfficialSC()
│ └── removeOfficialSC()
│
├── officialSystemUsers (mapping + array)
│ ├── isOfficialSystemUser(address) → bool
│ ├── getAllOfficialSystemUsers() → address[]
│ ├── addOfficialSystemUser()
│ └── removeOfficialSystemUser()
│
├── officialSystemEmployees (mapping + array)
│ ├── isOfficialSystemEmployee(address) → bool
│ ├── getAllOfficialSystemEmployees() → address[]
│ ├── addOfficialSystemEmployee()
│ └── removeOfficialSystemEmployee()
│
├── officialSystemAdmins (mapping + array)
│ ├── isOfficialSystemAdmin(address) → bool
│ ├── getAllOfficialSystemAdmins() → address[]
│ ├── addOfficialSystemAdmin()
│ └── removeOfficialSystemAdmin()
|-- officialBaseTreasuryAdmins (mapping + array)
│ ├── isOfficialBaseTreasuryAdmin(address) → bool
│ ├── getAllOfficialSystemAdmins() → address[]
│ ├── addOfficialSystemAdmin()
│ └── removeOfficialSystemAdmin()
│
├── Event: UpdateOfficials(...)
└── Modifier: onlyBTExecutives (via hasBTAdminRole on masterHub)
📌 supporting Recovery of accidental transfers
Recoverable provides a robust mechanism for recovering accidentally sent ETH and ERC-20 tokens, incorporating precise admin fee management and secure administrative control. It serves as a foundational contract, extended by Controller and all children like Token that also serve as ElasticTreasuryHub or Spoke contracts.
🎯 Refund Calculation Formula
The calculation to determine the refund amount is defined as follows:
TOTAL_LEFT_TO_REFUND = TOTAL_SENT - TOTAL_REVERSED REFUND_AMOUNT = TOTAL_LEFT_TO_REFUND |
This ensures that the contract never refunds more than the amount originally received from the sender and must be precluded by a manual call by an admin (mult-sig) who externally verified that the given (non-contract) user has those tokens on deposit - and that such token should not be there. For example, a sales contract that holds USDC - should the sales contract track who sent what USDC, and there is USDC being claimed as accidental, the admin would need to verify that such USDC was not part of the sale proceeds.
🎯 State Variables
- ADMIN_FEE_FIXED: Fixed fee required for each recovery transaction (0.01 ETH).
- totalAdminFeesCollected: Total ETH collected as admin fees.
- tokenContract: Address of the token contract used for admin checks.
- mapping(address => UserBalance) ethBalances: Tracks ETH sent and reversed for each address.
- mapping(address => mapping(address => UserBalance)) tokenBalances: Tracks tokens sent and reversed for each address and token.
struct ReversibleUserBalance { uint256 totalReceivedThatIsReversible; uint256 totalReversed; uint256 totalReversals; }
|
🔐 Access Control
- Uses a modifier onlyBTExecutives to restrict administrative functions to authorized that must be defined in the controller as “TreasuryAdmin” or “SmartContract”.
IController interface File IController.sol
/ SPDX-License-Identifier: Copyright 2025 // OFFICIAL DEL NORTE NETWORK COMPONENT // Designed and coded by: Ken Silverman // implementation help by Tony Sparks pragma solidity ^0.8.20;
interface IController { struct OfficialEntity { string fullNameOfEntityOrLabel; string nationalIdOfEntity; address pubAddress; bool active; uint256 blockNumber; }
function addOfficialEntity(string memory, address, string memory, string memory) external returns (bool); function addOfficialEntityNow(string memory, address, string memory, string memory) external returns (bool); function removeOfficialEntity(string memory, address) external returns (bool); function isOfficialEntity(string memory, address) external view returns (bool); function isOfficialEntityFast(bytes32, address) external view returns (bool); function isOfficialDoubleEntity(string calldata, address, string calldata, address, bool) external view returns (bool); function isOfficialTripleEntity(string calldata, address, string calldata, address, string calldata, address, bool) external view returns (bool); function getOfficialEntity(string calldata, address) external view returns (OfficialEntity memory); function getAllOfficialEntities(string calldata) external view returns (OfficialEntity[] memory); function init(address officialSmartContractAddress); } |
Recoverable File Recoverable.sol
// SPDX-License-Identifier: CLOSED LICENSE COPYRIGHT 2025
// OFFICAL DEL NORTE NETWORK COMPONENT
// Designed By Ken Silverman for Del Norte. Implementation help from Tony Sparks
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./IController.sol";
/// @title ReversibleRecoveryBase
/// @notice Base contract tracking / reversing accidental ETH/ERC20 transfers with admin authorization
abstract contract Recoverable {
// STRUCTS
struct ReversibleUserBalance {
uint256 totalReceivedThatIsReversible;
uint256 totalReversed;
uint256 totalReversals;
}
// -----------------------------
// STORAGE
// -----------------------------
address public controller; // Address of the Controller contract (must implement IController)
uint256 public ADMIN_FEE_FIXED = 10 ** 17; // 0.1 ETH
uint256 public totalAdminFeesCollected;
mapping(address => ReversibleUserBalance) public reversibleEthBalances;
mapping(address => mapping(address => ReversibleUserBalance)) public reversibleTokenBalances;
// -----------------------------
// EVENTS
// -----------------------------
event TransferReversed(address indexed user, uint256 refundAmount, address tokenSC, uint256 adminFee);
event AdminFeeUpdated(uint256 newFee);
event ControllerChanged(address newController);
modifier onlyBTExecutives() virtual {
bool temp = IController(controller).isOfficialEntity("TreasuryAdmin", msg.sender) ||
IController(controller).isOfficialEntity("SmartContract", msg.sender);
require(temp, "Unauthorized access");
_; // run the code block referencing this modifier
}
// -----------------------------
// CONSTRUCTOR
// -----------------------------
constructor(address _controller) {
require(_controller != address(0), "Controller address cannot be zero");
controller = _controller;
}
// -----------------------------
// EXTERNAL METHODS
// -----------------------------
function manualUpdateReversibleBalanceETH(address userAddress, uint256 amount)
external onlyBTExecutives {
reversibleEthBalances[userAddress].totalReceivedThatIsReversible += amount;
}
function manualUpdateReversibleBalanceERC20(address userAddress, uint256 amount, address tokenSC)
external onlyBTExecutives {
reversibleTokenBalances[tokenSC][userAddress].totalReceivedThatIsReversible += amount;
}
function reverseAccidentalETH() external payable {
require(msg.value >= ADMIN_FEE_FIXED, "Insufficient admin fee");
require(!IController(controller).isOfficialEntity("Registrar", msg.sender),
"Registrars/launchpads may not be allowed to reverse any amounts they send.");
ReversibleUserBalance storage balance = reversibleEthBalances[msg.sender];
uint256 refundAmount = balance.totalReceivedThatIsReversible - balance.totalReversed;
require(refundAmount > 0, "Nothing to refund");
// Update state before external call
balance.totalReversed += refundAmount;
balance.totalReversals += 1;
totalAdminFeesCollected += msg.value;
// Perform the external call
(bool success, ) = msg.sender.call{value: refundAmount}("");
require(success, "Ether transfer failed");
emit TransferReversed(msg.sender, refundAmount, address(0), msg.value);
}
function reverseAccidentalERC20(address tokenSC) external payable {
require(msg.value >= ADMIN_FEE_FIXED, "Insufficient admin fee");
require(!IController(controller).isOfficialEntity("Registrar", msg.sender),
"Registrars/launchpads may not reverse any amounts they send.");
ReversibleUserBalance storage balance = reversibleTokenBalances[tokenSC][msg.sender];
uint256 refundAmount = balance.totalReceivedThatIsReversible - balance.totalReversed;
require(refundAmount > 0, "Nothing to refund");
// Update state before external call
balance.totalReversed += refundAmount;
balance.totalReversals += 1;
totalAdminFeesCollected += msg.value;
// Perform the external call
IERC20(tokenSC).transfer(msg.sender, refundAmount);
emit TransferReversed(msg.sender, refundAmount, tokenSC, msg.value);
}
function changeAdminFee(uint256 newFee) external onlyBTExecutives {
ADMIN_FEE_FIXED = newFee;
emit AdminFeeUpdated(newFee);
}
function changeController(address remote) internal onlyBTExecutives {
controller = remote;
emit ControllerChanged(remote);
}
}
✅ Events
- TransferReversed: emitted upon successful recovery of ETH or tokens.
- AdminFeeUpdated: emitted when the admin fee is updated.
Controller File Controller.solElasticTreasuryHub overview
// SPDX-License-Identifier: CLOSED LICENSE COPYRIGHT 2025
// OFFICAL DEL NORTE NETWORK COMPONENT
// Designed By Ken Silverman for Del Norte. Implementation help from Tony Sparks
pragma solidity ^0.8.17;
import "./Recoverable.sol";
contract Controller is Recoverable {
event TreasuryCreated(address treasuryAddress, string label);
event UpdateOfficials(
address indexed entityAddress,
address indexed updater,
string entityType, // "SC,SystemUser,SystemAdmin,BTADmin, etc ..."
string action, // Add,Remove (nothing is actually removed, only active boolean changed)
string fullNameOfEntityOrLabel, // person's full name or "SC"
string nationalIdOfEntity, // registered to entity or SC address if entity is SC
uint256 blockNumber, // block number of the event
bool remainsActive
);
struct OfficialEntity {
string fullNameOfEntityOrLabel;
string nationalIdOfEntity; // (if entity is a smart contract, leave empty or provide some other detail here.)
address pubAddress; // registered to entity or SC address if entity is SC
uint256 blockNumber;
bool active;
}
struct OfficialEntityGroup {
mapping(address => OfficialEntity) entitiesByAddress; // Maps entity address to its details
address[] entityList; // List of entity addresses for iteration
}
struct AdminTypeRegistry {
mapping(string => bool) isRegistered; // internally a hash key but requires string for lookup
mapping(bytes32 => bool) isHashRegistered; // slightly faster pre-hahsed lookup
string[] registeredAdminTypes;
mapping(bytes32 => string) nameByHash; // Reverse lookup from hash to original string
}
mapping(string => OfficialEntityGroup) private officialEntityGroupsByType;
// NEW structure to SUPPORT validateAdminType
AdminTypeRegistry private adminTypeRegistry;
address public remoteController; // Hub address for admin verification - ALWAYS “this” for now
bytes32 public constant KECCAK_TREASURY_ADMIN = keccak256(bytes("TreasuryAdmin"));
bytes32 public constant KECCAK_SMART_CONTRACT = keccak256(bytes("SmartContract"));
// Warning! Any official SC can call addEntity directly. This is NOT a first line of defense against that.
// The only defense there is to make sure your official SCs have their own calls to this controller’s
// isOfficialEntity(“TreasuryAdmin”) for example BEFORE calling addEntity() or removeEntity()
// init() LOCKS this Controller to only allowing TreasAdmins to adding official SCs to begin with.
// REMOTE case now allows a smartContract caller unless tx.origin is used
modifier onlyBTExecutives() override {
bool temp = isOfficialEntityFast(KECCAK_TREASURY_ADMIN, tx.origin) ||
isOfficialEntityFast(KECCAK_SMART_CONTRACT, msg.sender);
require(temp, "Unauthorized access");
_; // run the code block referencing this modifier
}
// to meet the 24K limit, BaseTreasury is now instantiated FIRST - however its concept remains the same - as a BASE class
// for ElasticTreasuryHUB and ElasticTreasurySPOKE MasterHub is now an optional, later passed “controller” that must be
// a separate instantiatedController. This will NOT be used for our needs yet. In other words, controller is ALWAYS
// this for now.
constructor(address initialControllerAdmin) Recoverable(address(this)) {
remoteController = address(this); // formerly masterHub, controller is ALWAYS this for now. (see remotelyManage)
// **Reject if initialControllerAdmin is 0x0000 (formerly as parent, if has no TreasuryAdmin**
if (initialControllerAdmin == address(0)) {
revert("Controller: needs at least one TreasuryAdmin");
}
string[5] memory defaultTypes = ["TreasuryAdmin", "SmartContract", "SystemUser",
"SystemEmployee", "SystemAdmin"]; // Registrars/TokenAdmins added in Token contract
for (uint256 i = 0; i < defaultTypes.length; i++) {
adminTypeRegistry.isRegistered[defaultTypes[i]] = true;
adminTypeRegistry.isHashRegistered[keccak256(bytes(defaultTypes[i]))] = true;
adminTypeRegistry.registeredAdminTypes.push(defaultTypes[i]);
}
addOfficialEntityNow("TreasuryAdmin", initialControllerAdmin, "Presumed Multi-Sig platform exec", "Initial Execs");
// SEE ElasticTreasuryHub transferTreasury() no longer adds SPOKE as SC official - now added here …
// NO LONGER adding self as Official SC, because SELF is now standalone controller NOT parent due to 24K max mem.
emit TreasuryCreated(address(this), "Controller");
}
// caller MUST be a HUB or SPOKE because Controller is now deployed separately.
// HUB and spoke are no longer Controllers themselves. addedBTAdmin is optional, HUB and SPOKE pass address(0)
function init(address officialSmartContractAddress) external onlyBTExecutives {
addOfficialEntity("SmartContract",officialSmartContractAddress, "SC no name","SC no ID"); // may exist
}
// do not use except for use case as of yet not imagined would need to be called immediately via child constructor
function remotelyManage(address _controller) external onlyBTExecutives {
bool isRemotelyManaged = (_controller != address(0) && _controller != address(this));
if (isRemotelyManaged) {
remoteController = _controller;
}
else {
remoteController = address(this); // not used if isRemotelyManaged is false
}
changeController(remoteController);
}
// if master is self, it is the same as passing false, because SELF (local) WILL manage in that case.
// Any controller that is not self must be another instance of BaseTreasury
// just a formality, if not here, controller can always be cast to BaseTreasury, even if self.
// Ex: BaseTreasury(controller).someFunction … will always work whether self or another BaseTreasury instance.
function doesRemoteControllerManageAllOfficials() internal view returns (bool) {
return controller != address(this);
}
///
// OFFICIAL ENTITIES
//
//
// Dynamically validate an admin type, and optionally register new ones
function validateAdminType(string memory adminType) internal view {
if (!adminTypeRegistry.isRegistered[adminType]) {
revert("Invalid admin type");
}
}
function addAdminType(string memory adminType) internal returns (bool) {
if (!adminTypeRegistry.isRegistered[adminType]) {
// to support remote case, let a TreasuryAdmin add too, since “SmartContract” is not sensed for remote
// chained transaction caller: EOA ⇒ contract A == > contract B (here) ⇒ remote controller (A not sensed)
// require(isOfficialEntity(“SmartContract”,msg.sender),”Only official SC can add new Official type”);
bytes32 hash = keccak256(bytes(adminType));
adminTypeRegistry.isRegistered[adminType] = true;
adminTypeRegistry.isHashRegistered[hash] = true;
adminTypeRegistry.nameByHash[hash] = adminType;
adminTypeRegistry.registeredAdminTypes.push(adminType);
return true;
}
return false;
}
// Retrieve all registered admin types
function getRegisteredAdminTypes() external view returns (string[] memory) {
return adminTypeRegistry.registeredAdminTypes;
}
function addOfficialEntity(string memory adminType, address entityAddr, string memory label,
string memory nationalIdOrSC) public onlyBTExecutives returns (bool) {
if (doesRemoteControllerManageAllOfficials()) {
return Controller(controller).addOfficialEntity(adminType, entityAddr, label, nationalIdOrSC);
}
return addOfficialEntityNow(adminType, entityAddr, label, nationalIdOrSC);
}
// can be used INTERNALLY only, we have this so the constructor can add INITIAL EXECS!
function addOfficialEntityNow(string memory adminType, address entityAddr, string memory label,
string memory nationalIdOrSC) internal returns (bool) {
if (doesRemoteControllerManageAllOfficials()) {
// NO require to avoid extra external call, and no harm having some depth,
// looping would just run out of gas
// require(Controller(controller).controller == controller,”remote’s controller must equal itself”);
return Controller(controller).addOfficialEntity(adminType, entityAddr, label, nationalIdOrSC);
}
require(entityAddr != address(0), "Invalid entity address");
addAdminType(adminType); // 🔹 add admin type as necessary
OfficialEntityGroup storage group = officialEntityGroupsByType[adminType];
if (group.entitiesByAddress[entityAddr].active) {
return false;
}
group.entitiesByAddress[entityAddr] = OfficialEntity(
label, nationalIdOrSC, entityAddr, block.number, true
);
group.entityList.push(entityAddr);
emit UpdateOfficials(entityAddr, msg.sender, adminType, "Add", label, nationalIdOrSC, block.number,true);
return true;
}
function removeOfficialEntity(string calldata adminType, address entityAddr) external onlyBTExecutives returns (bool) {
if (doesRemoteControllerManageAllOfficials()) {
return Controller(controller).removeOfficialEntity(adminType, entityAddr);
}
require(entityAddr != address(0), "Invalid entity address");
validateAdminType(adminType); // 🔹 Ensure valid type
OfficialEntityGroup storage group = officialEntityGroupsByType[adminType];
if (!group.entitiesByAddress[entityAddr].active) {
return false;
}
// Soft-delete by marking inactive, but retain in list for history
group.entitiesByAddress[entityAddr].active = false;
emit UpdateOfficials(entityAddr, msg.sender, adminType, "Remove",
group.entitiesByAddress[entityAddr].fullNameOfEntityOrLabel,
group.entitiesByAddress[entityAddr].nationalIdOfEntity,
block.number,false);
return true;
}
function isOfficialEntityFast(bytes32 hashedAdminType, address entityAddr) public view returns (bool) {
if (doesRemoteControllerManageAllOfficials()) {
return Controller(controller).isOfficialEntityFast(hashedAdminType, entityAddr);
}
require(entityAddr != address(0), "Invalid entity address");
string memory adminType = adminTypeRegistry.nameByHash[hashedAdminType];
if (!adminTypeRegistry.isHashRegistered[hashedAdminType]) {
revert("Invalid admin type");
}
return officialEntityGroupsByType[adminType].entitiesByAddress[entityAddr].active;
}
function isOfficialEntity(string memory adminType, address entityAddr) public view returns (bool) {
if (doesRemoteControllerManageAllOfficials()) {
return Controller(controller).isOfficialEntity(adminType, entityAddr);
}
require(entityAddr != address(0), "Invalid entity address");
validateAdminType(adminType); // 🔹 Ensure valid type
return officialEntityGroupsByType[adminType].entitiesByAddress[entityAddr].active;
}
function getOfficialEntity(string calldata adminType, address entityAddr) external view
returns (OfficialEntity memory) {
if (doesRemoteControllerManageAllOfficials()) {
return Controller(controller).getOfficialEntity(adminType, entityAddr);
}
require(entityAddr != address(0), "Invalid entity address");
validateAdminType(adminType); // 🔹 Ensure valid type
OfficialEntity storage entity = officialEntityGroupsByType[adminType].entitiesByAddress[entityAddr];
require(entity.active, "Entity not found or inactive");
return entity;
}
function getAllOfficialEntities(string calldata adminType) external view returns (OfficialEntity[] memory) {
if (doesRemoteControllerManageAllOfficials()) {
return Controller(controller).getAllOfficialEntities(adminType);
}
validateAdminType(adminType); // 🔹 Ensure valid type
OfficialEntityGroup storage group = officialEntityGroupsByType[adminType];
uint256 totalEntities = group.entityList.length;
OfficialEntity[] memory allEntities = new OfficialEntity[](totalEntities);
uint256 counter = 0;
for (uint256 i = 0; i < totalEntities; i++) {
address entityAddr = group.entityList[i];
if (group.entitiesByAddress[entityAddr].active) {
allEntities[counter] = group.entitiesByAddress[entityAddr];
counter++;
}
}
return allEntities;
}
// saves a double call
function isOfficialDoubleEntity(string calldata adminType1, address entityAddr1,
string calldata adminType2, address entityAddr2, bool isAnd) public view returns (bool) {
if (isAnd) {
return isOfficialEntity(adminType1,entityAddr1) && isOfficialEntity(adminType2, entityAddr2);
}
else {
return isOfficialEntity(adminType1,entityAddr1) || isOfficialEntity(adminType2, entityAddr2);
}
}
function isOfficialTripleEntity( string calldata adminType1, address entityAddr1,
string calldata adminType2, address entityAddr2,
string calldata adminType3, address entityAddr3, bool isAnd
) external view returns (bool) {
if (isAnd) {
return isOfficialEntity(adminType1, entityAddr1) &&
isOfficialEntity(adminType2, entityAddr2) &&
isOfficialEntity(adminType3, entityAddr3);
}
else {
return isOfficialEntity(adminType1, entityAddr1) ||
isOfficialEntity(adminType2, entityAddr2) ||
isOfficialEntity(adminType3, entityAddr3);
}
}
//
// END OFFICIAL ENTITIES
//
// NO ETH can be received in this way. we dont WANT to receive accidental or unsolicited eth
// See below for a payable method for SALES purposes and OTHER purposes (see BELOW).
// receive() external payable { // <== DO NOT IMPLEMENT, LEAVE COMMENTED
// ethBalances[msg.sender].totalReceivedThatIsReversible += msg.value;
//}
// this method is called by PURCHASERS on the SALES contract or any other contract which acts as its own HUB
// no eth may be sent to this contract unless the child implements a receive() or fallback - NOT a good idea!
// This should be THE only way to receive ETH by persons (outside of the treasuryReceive methods
// reserved for treasury actions)
// Sales contract calls this for example after a purchase is verified by whitelist etc ...
}
Superstructure Overview
The Elastic Treasury Model uses a clear and hierarchical treasury structure to manage ETH and multiple ERC20 tokens separately and explicitly. The treasury mapping now has enhanced granularity, clearly distinguishing between various token balances for each smart contract (spoke).
Explanation of the Hierarchy:
- Top Layer: Maps each Spoke smart contract address to an AllCoinTreasury.
- Middle Layer (AllCoinTreasury): Holds individual treasury states per token, identified by their contract address, plus a dedicated state for ETH (which has no address, hence a separate field). The label at this level, the spoke-level, is unique.
- Bottom Layer (SingleCoinTreasuryState): Records detailed state information for a single token (ERC20 or ETH).
Example Scenario Illustration:
- TokenContract (0xToken) transfers tokens to a SalesContract (0xSales):
- This updates: treasury[0xSales].tokenTreasury[0xToken].totalTransferred += amount.
- ETH transfer to VestingContract (0xVesting):
This updates: treasury[0xVesting].ethTreasury.totalTransferred += ethAmount.
Isolation between reclaim and reversible.
- Isolation Principle:
Elastic treasury operations (treasuryTransfer, treasuryTransferETH, treasuryReclaim, etc.) do not interact or have anything to do with accidental reversible balances as serviced in Controller/Hub/Spoke independently). There is no mixing of accidental and direct transfers with deliberate treasury management transfers.
- Separate States:
The Elastic Treasury maintains its independent states for transfers and reclaims, distinct from Controller Hub/Spoke accidental reversibles
Treasury Hub Complete Hierarchical Structure:
ElasticTreasuryHub (inherits BaseTreasury)
|
+-- treasury (mapping of spoke smart contracts addresses)
|
+-- AllCoinTreasury
|
+-- ethTreasury (SingleCoinTreasuryState for ETH)
+-- tokenTreasury (mapping of token address to SingleCoinTreasuryState)
Example Visual:
treasury [SpokeAddress1]
├── ethTreasury
│ ├── totalTransferred: 10 ETH
│ └── totalReclaimed: 2 ETH
│
└── tokenTreasury [0xTokenAddress]
├── totalTransferred: 1000 tokens
└── totalReclaimed: 100 tokens
Example Reclaim
After a sale if not all tokens are sold, they may be reclaimed by the Token Contract from the Sales Contract and then from there moved into the LP contract for example. A spoke may not be a hub for the same token for which it is a SPOKE. Example: Sales contract is a SPOKE of the Token for DTV but a HUB for the Vesting contract on USDC for potential payouts of USDC to independent contractors.
Below is a clear explanation along with a diagram illustrating the three-stage reclaim workflow of DTV from token contract (HUB) to Sales contract (SPOKE):
- Stage 1 – Reclaim Request:
A Treasury admin initiates the process by calling the hub’s reclaimRequest() function. This request is the starting point for reclaiming tokens or ETH.
- Stage 2 – Spoke Invocation:
Once the hub processes the reclaim request, it forwards the call by invoking treasuryReclaim() on the appropriate spoke contract. This ensures that the spoke is aware of the reclaim operation.
- Stage 3 – Final Acknowledgment:
Finally, the spoke contract, upon processing the reclaim, calls back to the hub’s treasuryReceiveReclaimed() function. This callback confirms that the tokens (or ETH) have been reclaimed and updates the hub’s treasury records.
To support a contract being both a HUB and a SPOKE (like the Sales Contact) the Controller will be referenced twice, that’s ok. In each case the SAME SC is attempted to be added as a SmartContract entity into the Controller. No harm done. No diamond inheritance needed, no virtual.
COMPLETE HUB, SPOKE and TOKEN
Instructions: Make a file called IController.sol has interface called “Controller”. The fileName is to distinguish from the actual class fileName Controller.sol This interface is used only for the compiler for casting method signatures and must be named Controller without the “I”.
File IElasticTreasurySpoke.sol
// SPDX-License-Identifier: Copyright 2025 // OFFICIAL DEL NORTE NETWORK COMPONENT // Designed and coded by: Ken Silverman // implementation help by Tony Sparks pragma solidity ^0.8.20;
interface IElasticTreasurySpoke { function treasuryReceive(address tokenAddress, uint256 amount) external; function treasuryReceiveETH() external payable; function treasuryReclaim(address tokenAddress, uint256 amount) external returns (bool); function treasuryReclaimETH(uint256 amount) external; } |
ElasticTreasuryHub File ElasticTreasuryHub.sol
// SPDX-License-Identifier: Copyright 2025 // OFFICIAL DEL NORTE NETWORK COMPONENT // Designed and coded by: Ken Silverman // Implementation help by Tony Sparks pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./IController.sol"; // // ⇐ for method signatures only to allow casting during compilation. import "./IElasticTreasurySpoke.sol"; import "./Recoverable.sol";
abstract contract ElasticTreasuryHub is Recoverable {
event HubTreasuryEvent(address adminCaller, string msg, bool success,address smartContractTarget, uint256 amount, address tokenOrZero); event HubExecutiveTokensWithdrawalEvent(address adminCaller, address receiver, uint256 amt, string note, address tokenSC); event HubExecutiveEthWithdrawalEvent(address adminCaller, address receiver, uint256 amt, string note);
struct SingleCoinTreasuryState { string label; uint256 totalTransferred; uint256 totalReclaimed; uint256 totalTimesTransferred; uint256 totalTimesReclaimed; uint256[] failedReclaimAttemptAmounts; uint256[] bouncedReclaimAmounts; }
struct AllCoinTreasury { mapping(address => SingleCoinTreasuryState) tokenTreasury; SingleCoinTreasuryState ethTreasury; string label; }
mapping(address => AllCoinTreasury) public treasury;
// Declare this outside the functions, in your contract bytes32 private constant KECCAK_TREASURY_ADMIN = keccak256("TreasuryAdmin"); // Declare this outside the functions, in your contract bytes32 private constant KECCAK_SMART_CONTRACT = keccak256("SmartContract");
uint256 constant DUST = 0.01 ether; // elastic treasury events with gas that fail without reversion or require // controller reference is NOW stored in Recoverable
// if somehow anyone managed a call to a spoke reclaim, (in theory they can't) but if they did, then // that SC would have the authority to call the reclaim here as a SC exec. // therefore we require BOTH SC as spoke AND tx.origin to be Treas on reclaim modifier onlyTreas() { require(IController(controller).isOfficialEntityFast(KECCAK_TREASURY_ADMIN,msg.sender), "Unauthorized access"); _; // run the code block referencing this modifier }
// masterHub can no longer exist because BaseTreasury is a SINGLE entity, not inherited anymore. // in other words, the ENTIRE Del Norte system uses ONE Base Treasury instance as CONTROL PANEL. // Controller already has an initial admin, so do not add any more here. // Admin can call addEntity to add more. (controller has option to receive more in init() but not necessary. constructor(address _controllerAddress) Recoverable(_controllerAddress) { IController(_controllerAddress).init(address(this)); }
// EMERGENCY withdraw for admin controlled DIRECT withdrawals to a PERSON (not a smart contract) // this can ONLY happen for example, when the company wants to send some USDC or ETH from the Sales contract // to an EXECUTIVE of the company for the purposes of exchanging to USDC, as an example. // Such USDC can be spent on a capital expense or manually passed into the vesting pool. // ONLY HUBS can do this. For example, only contracts GENERATING or expected to RECEIVE // ETH or tokens from an external event (not from a HUB) should be declared as HUBS. function executiveWithdrawETH(address personAddress, uint256 amt, string memory note) external onlyTreas { emit HubExecutiveEthWithdrawalEvent(msg.sender,personAddress,amt,note); } function executiveWithdrawTokens(address personAddress, uint256 amt, address tokenSCAddress, string memory note) external onlyTreas { emit HubExecutiveTokensWithdrawalEvent(msg.sender,personAddress,amt,note,tokenSCAddress); }
function treasuryTransfer(address SCAddress, string memory allCoinLabel, address tokenAddress, string memory tokenLabel, uint256 amount) external onlyTreas returns (bool) { // SELF is added as official SC in parent constructor - and same for spoke AllCoinTreasury storage coinTreasury = treasury[SCAddress]; if (bytes(coinTreasury.label).length != 0) { require(keccak256(bytes(coinTreasury.label)) == keccak256(bytes(allCoinLabel)), "AllCoinTreasury label mismatch"); } else { coinTreasury.label = allCoinLabel; } SingleCoinTreasuryState storage tokenState = coinTreasury.tokenTreasury[tokenAddress]; if (bytes(tokenState.label).length != 0 && bytes(tokenLabel).length != 0) { require(keccak256(bytes(tokenState.label)) == keccak256(bytes(tokenLabel)), "Token label mismatch"); } else if (bytes(tokenState.label).length == 0 && bytes(tokenLabel).length != 0) { tokenState.label = tokenLabel; } require(isContract(SCAddress), "Invalid contract"); tokenState.totalTransferred += amount; tokenState.totalTimesTransferred++; // works on self? If tokenAddress is same as THIS contract's address will this work? IERC20(tokenAddress).transfer(SCAddress, amount); IElasticTreasurySpoke(SCAddress).treasuryReceive(SCAddress,amount); // ⇐ makes spoke aware of transfer // Add to smartContractAdmins array if not already included // Spoke already added, this is in case spoke used different master (see BaseTreasury) IController(controller).addOfficialEntity("SmartContract",SCAddress,allCoinLabel,"Spoke SC"); emit HubTreasuryEvent(msg.sender,"Amount transferred to SC",true,SCAddress,amount,address(0)); return true; }
function treasuryTransferETH(address payable SCAddress, string memory allCoinLabel, string memory ethLabel, uint256 amount) external onlyTreas returns (bool) { AllCoinTreasury storage coinTreasury = treasury[SCAddress]; if (bytes(coinTreasury.label).length != 0) { require(keccak256(bytes(coinTreasury.label)) == keccak256(bytes(allCoinLabel)), "AllCoinTreasury label mismatch"); } else { coinTreasury.label = allCoinLabel; } SingleCoinTreasuryState storage ethState = coinTreasury.ethTreasury; if (bytes(ethState.label).length != 0 && bytes(ethLabel).length != 0) { require(keccak256(bytes(ethState.label)) == keccak256(bytes(ethLabel)), "ETH label mismatch"); } else if (bytes(ethState.label).length == 0 && bytes(ethLabel).length != 0) { ethState.label = ethLabel; } require(address(this).balance >= amount, "Insufficient ETH balance"); ethState.totalTransferred += amount; ethState.totalTimesTransferred++; // DO NOT USE THIS ⇒ (bool success,) = SCAddress.call{value: amount}(""); // DO NOT USE THIS ⇒ require(success, "ETH Transfer failed"); // treasRcvEth is PAYABLE!!! So ... // DO NOT CALL transfer DO NOT USE "call" and // DO NOT have receive() method IN the SPOKE IElasticTreasurySpoke(SCAddress).treasuryReceiveETH{value: amount}(); // ⇐ TRANSFERS TO SPOKE // Add to smartContractAdmins array if not already included IController(controller).addOfficialEntity("SmartContract",SCAddress,allCoinLabel,"Spoke SC"); //⇐ NOT in MASTER emit HubTreasuryEvent(msg.sender,"Amount transferred to SC",true,SCAddress,amount,address(0)); return true; }
// ETH reclaim receiver function treasuryReceiveReclaimedETH() external payable returns (bool) { AllCoinTreasury storage coinTreasury = treasury[msg.sender]; require(bytes(coinTreasury.label).length != 0, "Treasury entry does not exist for caller as SPOKE"); require(IController(controller).isOfficialDoubleEntity("SmartContract",msg.sender, "TreasuryAdmin",tx.origin,true), "Originator must be treas and sender must be an official smart contract."); SingleCoinTreasuryState storage ethState = coinTreasury.ethTreasury; ethState.totalReclaimed += msg.value; ethState.totalTimesReclaimed++; emit HubTreasuryEvent(msg.sender, "ETH reclaimed", true, msg.sender,msg.value,address(0)); return true; }
// Token reclaim notifier function treasuryReceiveReclaimedTokens(address tokenAddress, uint256 amt) external returns (bool) { AllCoinTreasury storage coinTreasury = treasury[msg.sender]; require(bytes(coinTreasury.label).length != 0, "Treasury entry does not exist"); require(IController(controller).isOfficialDoubleEntity("SmartContract",msg.sender, "TreasuryAdmin",tx.origin,true), "Originator must be treas and sender must be an official smart contract."); SingleCoinTreasuryState storage tokenState = coinTreasury.tokenTreasury[tokenAddress]; tokenState.totalReclaimed += amt; tokenState.totalTimesReclaimed++; emit HubTreasuryEvent(msg.sender, "Token reclaimed", true, address(this),amt,tokenAddress); return true; }
function treasuryReclaimRequest(address SCAddress, address tokenAddress, uint256 amount) external onlyTreas returns (bool) { uint256 availableToReclaim = treasury[SCAddress].tokenTreasury[tokenAddress].totalTransferred - treasury[SCAddress].tokenTreasury[tokenAddress].totalReclaimed; if (amount > treasury[SCAddress].tokenTreasury[tokenAddress].totalTransferred) { treasury[SCAddress].tokenTreasury[tokenAddress].failedReclaimAttemptAmounts.push(amount); emit HubTreasuryEvent(msg.sender, "Reclaim request exceeds total transferred", false, address(this),amount,address(0)); return false; } if (amount > availableToReclaim) { treasury[SCAddress].tokenTreasury[tokenAddress].bouncedReclaimAmounts.push(amount); amount = availableToReclaim - DUST; emit HubTreasuryEvent(msg.sender, "Reclaim request bounce, too high", false, address(this),amount,address(0)); return false; } // if this fails, whole thing will revert, but the idea is if we passed // the above checks, than the corresponding state in the spoke should match. // In other words a revert should never happen if we get to this point. // if it does there is a state mismatch that the web manager will have to // warn about for manual reconciliation. // eth logs on chain should group msg.sender so when reclaim succeeds the message // will follow this event message herein 'request submitted'. If however, // the transfer fails due to rejection at the spoke, // the below log entry will never be entered. IElasticTreasurySpoke(SCAddress).treasuryReclaim(tokenAddress, amount); treasury[SCAddress].tokenTreasury[tokenAddress].totalReclaimed += amount; treasury[SCAddress].tokenTreasury[tokenAddress].totalTimesReclaimed++; emit HubTreasuryEvent(msg.sender, "Reclaim TOKEN request submitted to SPOKE", true,SCAddress,amount,tokenAddress); return true; }
function treasuryReclaimRequestETH(address payable SCAddress, uint256 amount) external onlyTreas returns (bool) { uint256 availableToReclaim = treasury[SCAddress].ethTreasury.totalTransferred - treasury[SCAddress].ethTreasury.totalReclaimed; if (amount > treasury[SCAddress].ethTreasury.totalTransferred) { treasury[SCAddress].ethTreasury.failedReclaimAttemptAmounts.push(amount); emit HubTreasuryEvent(msg.sender, "ETH Reclaim exceeds total transferred", false,SCAddress,amount,address(0)); return false; } if (amount > availableToReclaim) { treasury[SCAddress].ethTreasury.bouncedReclaimAmounts.push(amount); amount = availableToReclaim - DUST; emit HubTreasuryEvent(msg.sender, "ETH Reclaim request bounce, too high", false,SCAddress,amount,address(0)); return false; } IElasticTreasurySpoke(SCAddress).treasuryReclaimETH(amount); treasury[SCAddress].ethTreasury.totalReclaimed += amount; treasury[SCAddress].ethTreasury.totalTimesReclaimed++; emit HubTreasuryEvent(msg.sender, "ETH reclaim request submitted", true,SCAddress,amount,address(0)); return true; }
// PURCHASERS on the SALES contract or any other contract which acts as its own HUB // should not accidently get ETH sent to this contract unless the child implements a receive() // or fallback or other payable - NOT a good idea (except buy and other planned custom methods) ! // There should be no way to receive ETH (or tokens if it were possible) outside of SALES or other means // Sales contract has its own payable for example after a purchase is verified by whitelist etc ... // function customReceiveETH internal virtual payable { do NOT implement
function isContract(address account) internal view returns (bool) { return account.code.length > 0; }
} |
File IElasticTreasuryHub.sol
// SPDX-License-Identifier: Copyright 2025 // OFFICIAL DEL NORTE NETWORK COMPONENT // Designed and coded by: Ken Silverman // Implementation help by Tony Sparks pragma solidity ^0.8.20;
interface IElasticTreasuryHub { function withdrawETHToPerson(address personAddress, uint256 amt, string calldata note) external; function withdrawTokensToPerson(address personAddress, uint256 amt, address tokenSCAddress, string calldata note) external; function treasuryTransfer(address SCAddress, string calldata allCoinLabel, address tokenAddress, string calldata tokenLabel, uint256 amount) external returns (bool); function treasuryTransferETH(address payable SCAddress, string calldata allCoinLabel, string calldata ethLabel, uint256 amount) external returns (bool); function treasuryReceiveReclaimedETH() external payable returns (bool); function treasuryReceiveReclaimedTokens(address tokenAddress, uint256 amt) external returns (bool); function treasuryReclaimRequest(address SCAddress, address tokenAddress, uint256 amount) external returns (bool); function treasuryReclaimRequestETH(address payable SCAddress, uint256 amount) external returns (bool); } |
ElasticTreasurySpoke File ElasticTreasurySpoke.sol
// SPDX-License-Identifier: Copyright 2025 // OFFICIAL DEL NORTE NETWORK COMPONENT // Designed and coded by: Ken Silverman // Implementation help by Tony Sparks pragma solidity ^0.8.20; import "./IController.sol"; import "./IElasticTreasuryHub.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./Recoverable.sol";
contract ElasticTreasurySpoke is Recoverable {
event SpokeReceivedEth(address sender, uint256 amount); event SpokeReceivedTokens(address sender, uint256 amount, address tokenSCAddress); event SpokeReclaimEthEvent(address origin, address hubContract, string msg, uint256 amount); event SpokeReclaimTokensEvent(address origin, address hubContract, address tokenAddress, string msg, uint256 amount);
address public hubTokenAddress;
struct SpokeTreasuryEntry { uint256 totalReceived; uint256 totalReclaimed; }
mapping(address => SpokeTreasuryEntry) public spokeTreasury; // tokenAddress => SpokeTreasuryEntry SpokeTreasuryEntry public spokeTreasuryETH; // ETH treasury entry explicitly
// Controller already has an initial admin, so do not add any more here. constructor(address _controller, address _hubTokenAddress) Recoverable(_controller) { IController(controller).init(address(0)); hubTokenAddress = _hubTokenAddress; }
function treasuryReceive(address tokenAddress, uint256 amount) external { require(msg.sender == hubTokenAddress, "sender must be HUB"); spokeTreasury[tokenAddress].totalReceived += amount; // explicitly tracks ERC20 tokens received from treasuryTransfer }
// ETH transfers automatically trigger receive() only if no data is sent. // Here, we're explicitly defining a method (treasuryReceiveETH) // that's explicitly called by the hub to transfer ETH and record this separately // treasury-related ETH separately from ONE CALL to THIS METHOD. NO PRIOR TRANSFER! function treasuryReceiveETH() external payable { // ⇐ MUST DIRECTLY RECEIVE NOW require(msg.sender == hubTokenAddress,"sender must be HUB"); // confirm amount actually equals msg.value spokeTreasuryETH.totalReceived += msg.value; emit SpokeReceivedEth(msg.sender,msg.value); }
// DO NOT USE ⇒ receive() external payable { // NO NO NO NO receive method! Do NOT allow incoming ETH // emit SpokeReceivedEth(msg.sender,msg.value); // except by treasRcvETH // }
// ANY address that this contract has deployed as its HUB is 100% trusted. // because caller must be hub and HUB is only accessible by TREAS. function treasuryReclaim(address tokenAddress, uint256 amount) external returns (bool) { require(spokeTreasury[tokenAddress].totalReceived >= amount, "Low balance to reclaim on"); require(hubTokenAddress == msg.sender); spokeTreasury[tokenAddress].totalReclaimed += amount; IERC20(tokenAddress).transfer(msg.sender, amount); IElasticTreasuryHub(msg.sender).treasuryReceiveReclaimedTokens(tokenAddress,amount); emit SpokeReclaimTokensEvent(tx.origin, msg.sender, tokenAddress, "Spoke sent Reclaim Tokens back to HUB", amount); return true; }
// msg.sender must be the HUB contract address here. For safety, verify that. // no officialSmartContract entity is required here, HUB is 100% trusted as caller. // because HUB can only be called by TREAS. function treasuryReclaimETH(uint256 amount) external { require(spokeTreasuryETH.totalReceived >= amount, "Insufficient received balance"); require(hubTokenAddress == msg.sender); spokeTreasuryETH.totalReclaimed += amount; IElasticTreasuryHub(payable(msg.sender)).treasuryReceiveReclaimedETH{value : amount}(); // ⇐MAKES TRANSFER emit SpokeReclaimEthEvent(tx.origin, msg.sender, "Spoke sent Reclaim ETH back to HUB", amount); } } |
Security and Governance
- Multi-sig Executive Control: Only executives can perform treasury transfers and reclaim operations.
- Contract Validation: All transfers validate contract authenticity and method implementation.
- Limits Enforcement: Contracts cannot exceed their allocated maximum.
- Transparency: Full transaction history recorded within the hub and each spoke contract.
The Elastic Treasury model provides a structured, secure, and decentralized method of managing token allocations among interconnected smart contracts. The Hub-and-Spokes model, with clearly defined treasuryReceive and treasuryReclaim mechanisms, ensures robust control, transparency, and operational efficiency in blockchain token management. Child contracts extend Recovery and implement an interface version of Controller for casting only. stored in IController.sol)
🧩 Integration with Token Contract and Elastic Treasury
Contracts like the Token Contract and Sales Contract point to our Controller via ElasticTreasuryHub or ElasticTreasurySpoke both of which point to a Controller
📄 Implementation Examples
Token Contract Example
contract TokenContract is ElasticTreasuryHub { // For token contract, no initialAdmin necessary. It is already defined in // Controller at deploy. Just pass controllerAddress. // constructor(address controllerAddress)ElasticTreasuryHub (controllerAddress,address[0]) {} } |
Sales Contract Example (Implementing Treasury and Recovery)
contract SalesContract is ElasticTreasuryHub, ElasticTreasurySpoke { // master gets passed up the chain to Controller via the // hub and spoke contract constructors respectively. constructor(address _controllerAddress, address _hubContract) ElasticTreasuryHub(_controllerAddress); ElasticTreasurySpoke(_hubContract,_controllerAddress) {}
// hub and spoke methods fully implemented in HUB and SPOKE
}
|
|
Whitelisted Users Map in Token Contract
Key-Value Structure
- Example: 0x123456789abcdef...
- Value: Registration Note String, formatted to include essential details for user verification and permissions.
Registration Note Format
Registrars are stored in controller as address -> label where label includes the registrarName and the registrar’s public key. Each registration note consists of multiple fields concatenated with underscores (_) for structured parsing.
Fields Included:
- Registrar Full Name
- Registrar ID
- Registrant ID (unique registration number for user)
- ISO 3166-1 alpha-3 Country Code
- Approval Limit in USD (fixed at 8000 USDC)
- Registration Origin Purpose “Sales-Event-2025”
- Permission Level
- Reserved Byte
Formatted JSON String Example:
"{regstrName : AcmeCorp, regId: 12345, userId : 5899425337, cntry : MEX, limUSDC : 8000, purpose : “Sales Event 2025”, perm : 2, res : 00}
Field Breakdown |
Field | Example | Description |
Registrar Name | AcmeCorp | The registrar's name. |
Registrar ID | 12345 | Unique identifier for the registrar. |
Country Code | USA | ISO 3166-1 alpha-3 country code (e.g., "USA" for the United States). |
Approval Limit | 50000 | Maximum approval amount in USD. |
Permission Level | 2 | Indicates the user is an accredited investor in the US. |
Reserved Byte | 00 | Reserved for future functionality. |
Permission Levels
- 1 → Non-US users.
- 2 → Accredited US investors.
- 3 and higher → Reserved for future use.
Application
This structured format ensures that all KYC, user verification and permissions data is 3rd party publicly verified and publicly accessible via the user's Ethereum address.
✔️ Sales Contract Access: Purchases require approval and accreditation/residency status.
✔️ Compliance with Standard 1404: Ensures a user is whitelisted for secure transactions.
✔️ Efficient Verification: Eliminates extra gatekeeping steps, ensuring secure transaction processing.
Final Notes
- While the approval limit is irrelevant for regular transfers, ensuring the user is whitelisted remains crucial.
- This approach allows secure and compliant transactions in a streamlined manner.
- Enforces a “TGE start” time, and tracks how many tokens a user already holds to cap their total token allotment (based on their whitelisted/approved max).
Sales Contract notes
- Accepts both ETH and USDC (or other stablecoins/tokens) in exchange for the new tokens.
- Owner pre-TGE method to set the token price in USDC (default = $0.04) and optionally in ETH (bypassing oracles).
- The signature-based whitelisting approach to ensure only addresses that present a valid signature (from the contract owner), i.e. the registration key model has been replaced with a WhitelistedUsers model because registrars do not provide PGP type signatures, only IDs. In other words registrars to date do not provide registration keys, just IDS. The model we are now using is expressed here:
Vesting Contract
- Allows the owner to create multiple vesting schedules for an address that can merge if startBlock, beneficiary and tokenContractAddress are the same.
- Each vesting schedule must have its own parameters (e.g., cliff, first payment total monthly payments, totalValue) and can be MERGED to existing schedules with the *same* params.
- Schedule: totalNumberOfPayments, startBlock, lastClaimedBlock, amtPerPayment, payInterval, claimedPayments
- MERGE can only occur if lastClaimedBlock is ZERO and startBlock is same.
- startBlock of zero means use whatever startBlock is ALREADY there.
- payInterval is FIXED at about 30 days: 216,000 blocks (for flexibility so store in schedule)
- Spends from whatever is put there via its elastic treasury HUB (the Token for team/purchasers)
- The SALES contract for USDC (which will require a separate deployment of another vesting contract because a SPOKE cannot support two HUBS).
- Has the means to send DTV or USDC to independent contractors.
Liquidity Pool Contract
- Allows a portion of fees to be taken from the reserve pool in tranches to purchase tokens from a gated liquidity pool, providing locked tokens to a membership contract.
Membership Contract
- DTVT is a utility token that is purchased on behalf of users of the platform to pay for their use of the service, such as paying taxes, or transferring rights of their NFT to another entity. Users of the service will receive membership tokens in a special locked Membership contract that maps the user to a membershipLevel which is exactly equal to the number of tokens purchased on their behalf. Membership tokens are never in the possession of the user. They are effectively, receipts for services, and the more receipts had, the higher the membership level. The user cannot transfer these tokens because they are never in custody of them. The tokens are purchased on their behalf to pay the fee for use and is meant to act as receipt for services, conferring both loyalty points and membership at the Membership for services design is forthcoming.
STAKING and REWARDS Contract - the Validator Pool
- Implements a novel non-staking rewards mechanism, where token holders can stake to a Validator Pool contract (aka Rewards Contract). Claiming is only possible by performing a validation action.
- Users call ClaimRewards() with a novel tranched summing algorithm. - but such rewards is NOT payable until the user performs an action that provides badges to existing users of hte system - the more the reward, the more badges must be provided to as of yet unbadged owners - if all owners are badges, they ca receive updated badges where the badge structure contains the lates date of verification and the number of verificfations historically
2.1 TOKEN Contract
- All 850,000,000 Tokens Minted Upfront
- Derives from ERC-20
- Transfer restricted to registration keys however this prevents possible legitimate transfers between private individuals? Only receiver needs registration key?
- Pre-mints entire supply directly inside the token contract where it remains deployed with a single admin address into the admins array (a multi-sig Gnosis Safe address)
- Elastic Treasury Calls from that admin address allocate to different contracts on demand. Admin can also add other multi-sig admins to perform these actions.
- From that pre-minted supply, admin manually calls (e.g., 50%) treasuryTransfer() of up to 50% to the Vesting Contract before the TGE starts (Team, Sale, Founders, devs …).
- If the vesting contract doesn’t add schedules forl all of those tokens, admin can call treasuryReclaim()) at anytime to return remainder back to the Token Contract which serves as the CORE_TREASURY.
- Allocation of DTV for Team & Founders & USDC payout schedules to developers.
- The vesting details (cliffs, schedules) are handled in the Vesting contract, typically by calling VestingContract.createSchedule(...).
-
ERC20Burnable: Enables a portion of tokens purchased from liquidity pool by the NFT contract for membership fees to burn (destroy) tokens, reducing total supply. Liikely no burn for us because the membership contract takes its place. Therefore there will not need to be a specific burn() method that only Smart contracts in the smartContractAdmins array can call.
- intialAdmin This is already passed into the Controller. Must be at least one such “executive” address in the Controller. That address has rights to call the elastic treasury functions which replaces functions like transferToSale(...), transferToVest(...) with a more flexible paradigm: treasuryTransfer(labelName, SmartContractAddress, Amount);
- Elastic treasury hub and spoke model achieves through treasuryTransfer() by a multi-sig ADMIN.
- Gated Transfer under 1404 no longer use a registrationKey keccack(JumPubKey,userPubKey,auth=1) model, REPLACED by whitelistedUsers map as earlier described because it is our understanding that registrars still do not provide public keys to date.
- However, regKey is still needed for user to register himself without our paying gas fees. This is achieved when a separate server we control pings a Registrar to verify a user’s pubAddress. If they pass our registration a callback of the ISO country of origin is returned, else restricted.
- removeTransferRestriction(accessRole Admin) After 12 months this restriction will (possibly) be removed.
TOKEN contract
// SPDX-License-Identifier: Copyright 2025 // OFFICIAL DEL NORTE NETWORK COMPONENT // Designed and coded by: Ken Silverman // Implementation help by Tony Sparks pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import "./ElasticTreasuryHub.sol"; import "./IController.sol";
contract DelNorteToken is ERC20, ERC20Burnable, ElasticTreasuryHub {
event RegistrarEvent(address indexed registrar, uint256 blockNumber, string action, uint256 numUsers);
struct WhitelistEntry { address user; string note; }
uint256 public constant MAX_SUPPLY = 850_000_000 * 10**18; bool public gatingActive = true;
mapping(address => string) public whitelistedUsers; mapping(address => bool) public isUserRestricted; address[] public whitelistedUserList;
// if masterHub is 00000000000 it means SELF (pass it up the line) // address baseTreasuryAddress, address intialAdmin constructor(address _controller, address initialAdmin) ERC20("Del Norte Token", "DTV") ERC20Permit("Del Norte Token"); ElasticTreasuryHub(_controller) { require(_controller != address(0), "Controller address cannot be zero"); bool areLords = IController(_controller).isOfficialEntity("TreasuryAdmin",initialAdmin); require(areLords, "First token admin (multi-sig execs) must be initial Controller TreasAdmin."); _mint(address(this), MAX_SUPPLY); // Can we as admins be registrar? Yes if we provide KYC // BaseTreasury must mark this SC as SmartContract already // These calls assume controller is of type IController or castable IController(controller).addOfficialEntityNow("Registrar", initialAdmin, "ExecGrp", "acting Registrar"); IController(controller).addOfficialEntityNow("TokenAdmin", initialAdmin, "ExecGrp", "initTknAdmin"); }
modifier onlyTokenExecutives() { require(IController(controller).isOfficialEntity("TokenAdmin",msg.sender), "NotTknExec"); _; }
// no LP or other retrieval to any non-registered user. Only Non-US can register. function isRestricted(address sender, address receiver) public view returns (bool) { if ( !gatingActive || IController(controller).isOfficialTripleEntity("TreasuryAdmin", sender,"SmartContract", sender, "SmartContract", receiver,false) ) { return false; } return !isUnrestrictedWhitelistedUser(receiver); }
function isUnrestrictedWhitelistedUser(address user) public view returns (bool) { return bytes(whitelistedUsers[user]).length > 0 && !isUserRestricted[user]; }
function transfer(address recipient, uint256 amount) public override returns (bool) { require(!isRestricted(msg.sender, recipient), "Transfer restricted"); return super.transfer(recipient, amount); }
function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) { require(!isRestricted(sender, recipient), "Transfer restricted"); return super.transferFrom(sender, recipient, amount); }
function removeGating() external { require(IController(controller).isOfficialEntity("TokenAdmin",msg.sender)); gatingActive = false; }
// NOT just for sales but for regular users who are registered function addWhitelistedUser(address user, string memory registrationNote) external { require(IController(controller).isOfficialEntity("Registrar", msg.sender), "Not Registrar"); whitelistedUsers[user] = registrationNote; whitelistedUserList.push(user); emit RegistrarEvent(msg.sender, block.number, "Added a User", 1); }
function deleteWhitelistedUser(address user) external { require(IController(controller).isOfficialEntity("Registrar",msg.sender)); delete whitelistedUsers[user]; emit RegistrarEvent(msg.sender, block.number, "Deleted a User", 1); }
function getAllWhitelistedUsers() external view returns (address[] memory) { return whitelistedUserList; }
function addWhitelistedUsers(WhitelistEntry[] calldata entries) external { require(IController(controller).isOfficialEntity("Registrar", msg.sender), "Not Registrar!"); uint256 numUsers = 0; for (uint256 i = 0; i < entries.length; ++i) { address user = entries[i].user; if (bytes(whitelistedUsers[user]).length == 0) { whitelistedUserList.push(user); numUsers++; } whitelistedUsers[user] = entries[i].note; } emit RegistrarEvent(msg.sender, block.number, "Added Users", numUsers); }
function restrictWhitelistedUsers(address[] calldata users) external { require(IController(controller).isOfficialEntity("Registrar", msg.sender), "Not Registrar"); uint256 numUsers = 0; for (uint256 i = 0; i < users.length; ++i) { address user = users[i]; if (bytes(whitelistedUsers[user]).length != 0 && !isUserRestricted[user]) { isUserRestricted[user] = true; numUsers++; } } emit RegistrarEvent(msg.sender, block.number, "Users Restricted", numUsers); }
function reactivateWhitelistedUsers(address[] calldata users) external { require(IController(controller).isOfficialEntity("Registrar", msg.sender), "Not Registrar"); uint256 numUsers = 0; for (uint256 i = 0; i < users.length; ++i) { address user = users[i]; if (bytes(whitelistedUsers[user]).length != 0 && isUserRestricted[user]) { isUserRestricted[user] = false; numUsers++; } } emit RegistrarEvent(msg.sender, block.number, "Users Reactivated", numUsers); }
function getActiveWhitelistedUsers() external view returns (address[] memory) { uint256 activeCount = 0; for (uint256 i = 0; i < whitelistedUserList.length; ++i) { if (!isUserRestricted[whitelistedUserList[i]]) { activeCount++; } } address[] memory activeUsers = new address[](activeCount); uint256 j = 0; for (uint256 i = 0; i < whitelistedUserList.length; ++i) { address user = whitelistedUserList[i]; if (!isUserRestricted[user]) { activeUsers[j++] = user; } } return activeUsers; }
/** * @notice Allows a user to self-register (whitelist themselves) using an off-chain signature * provided by an approved Registrar. The registrar signs a hashed message consisting of: * * keccak256(abi.encodePacked(userAddress + "_" + thisTokencontractAddress)) * * - The registration note (string) passed to this function is NOT part of the signed message. * It is for reference and display only and can be spoofed. All actual verification (e.g., KYC, * ISO country code, etc.) is done off-chain by registrar, details stored off-chain by address. * * - The registrar's address must be registered in the controller as an official entity * of type "Registrar". registrarSignature = regKey * - The user (msg.sender) pays the gas and must be the subject of the signed message. */ function whitelistUserWithRegKey(address registrarAddress, string calldata registrationNote, bytes calldata registrarSignature) external { require(bytes(whitelistedUsers[msg.sender]).length == 0, "User already registered"); // Message hash format: keccak256(user + this contract) // removed underscore fixed length 160 bytes32 messageHash = keccak256(abi.encodePacked(msg.sender, address(this))); // removed underscore because addresseses are fixed length and this we we can avoid use of hextostring //bytes memory rawMessage = abi.encodePacked(Strings.toHexString(uint160(msg.sender), 20), // "_",Strings.toHexString(uint160(address(this)), 20)); //bytes32 messageHash = keccak256(rawMessage); // Ethereum signed message format bytes32 ethSignedMessageHash = keccak256( abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash) ); // Recover signer address recoveredSigner = recoverSigner(ethSignedMessageHash, registrarSignature); require(recoveredSigner == registrarAddress, "Signature not from registrar"); require(IController(controller).isOfficialEntity("Registrar", registrarAddress), "Registrar not approved"); // Whitelist the user whitelistedUsers[msg.sender] = registrationNote; whitelistedUserList.push(msg.sender); emit RegistrarEvent(registrarAddress, block.number, "User Self-Registered", 1); }
// Internal helper to recover the signer from signature function recoverSigner(bytes32 hash, bytes memory signature) internal pure returns (address) { require(signature.length == 65, "Invalid signature length"); bytes32 r; bytes32 s; uint8 v;
assembly { r := mload(add(signature, 32)) s := mload(add(signature, 64)) v := byte(0, mload(add(signature, 96))) }
if (v < 27) { v += 27; }
require(v == 27 || v == 28, "Invalid v value"); return ecrecover(hash, v, r, s); }
}
|
- By embedding the restrictive nature of transfer in the token contract itself, the Liquidity Pool Contract is able to be pure Uniswap factory created. But a uniswap trader may get an error message unless they trade through our website swap protocol because transferFrom will likely fail unless the person interacting with the contract is registered. The liquidity pool acts as our INTERNAL CORPORATE TREASURY and is intended to facilitate purchases for users of our service.
- If a registered user interacts with the liquidity pool, such interaction is intended to emulate a direct sale to or buyback from a registered user from Del Norte - as an internal sale - not advertised for this purpose but nonetheless requiring user to be a registered non-US-resident according to our 3rd party registrar and stored as a whitelisted user.
* - Essentially, the Safe is `msg.sender` (the on-chain address) once the necessary approvals are
* collected. The contract sees the Safe as a single address with the roles.
*
*/
Key Points
-
We have a capped total supply of 850,000,000 tokens.
- The mint function can only be called by the contract owner.
- The entire cap is minted up front. =- be sure to update coinMarketCap with uncirclated addresses - such as the membership contract.
2.2 SALE Contract
Handles the following in a production-minded manner:
- treasuryTransfer() zero Allocated tokens for the sale - since sale contract only adds vesting schedules, It is a Token spoke only to become an official SmartContract entity.
- Purchases using ETH and/or stablecoins (e.g. USDC).
- Whitelisting via a call to the token contract registerUser(user,registrarNotes)
- Security enhancements (Reentrancy guard, officalEntity checks and no upgradeability).
- Future Extensibility - none. Unsold tokens are reclaimable by token contract.
- TreasuryAdmin of Token contract transfers 25% of tokens upfront to the Sale contract.
- Contract-held tokens minimizes gas for end users.
- Ensures predictable supply.
2.2.1. Overview of the Approach
Token flow management
Deploy the ERC20 token with a total supply minted internally.
- Transfer 50% of tokens from TOKEN ⇒ VESTING
- When a registered buyer purchases, the contract ADDS a SCHEDULE from its balance to the vesting schedule with permissions as a “SmartContract” entity to do so. addSchedule() accepts only “TreasuryAdmin” or “SmartContract” officials (onlyBTExecutives modifier of the Controller). A Registrar may NOT directly add a schedule! They may give us a list that we would add as an Admin after confirming the appropriate USDC has been transferred to the SmartContract by a confirmed Registrar address.
- Transfer method must be kept private.
- Buy method should be merged into one buy() method.
- A changeDepositAddress can be added but callable only by the owner of the -_depositAddress entered in the constructor at deploy. For now it is suggested no such method is added.
SALES CONTRACT NEEDS
- startBlockReg and endBlockReg (by blockNumber) isOnlyBTxecutives (emits event)
- startSaleReg and endSaleReg (by blockNum) isOnlyBTExecutives (emits event)
- registerUserForSale() requires Controller(controller). isOfficialEntity(“Registrar”) . Calls token’s whitelistUser with a registration Note specifically noting a RegistrationNote of: “Registrar Address” (msg.sender), “source: sales”, amt is fixed at $8,000 and specified in dollars. “ISO 3-char code for Country of residence”, etc …
- buyWithEth() buyWithUSDC() specifically checks that user is whitelisted via a call to Token contract’s isWhitelistedUser() method and that RegistrationNote has an amt greater than the equivalent ETH (using an oracle) or actual USDC sent. Upon confirmation calls VestingContract’s addSchedule() ... THEREFORE vesting contract must be deployed before Sales Contract.
- OPTIONAL: buyWithEthUsingRegKey() buyWithUSDCUsingRegKey() accepts a registration key and then we use ECDSA.ecrecover to verify the signature from kycSigner (JumPubKey) against a known message. In that event the buyWithRegKey() method accepts (regKey, pubKey,JumPubKey, maxTokenLimit, amountIfUSDC), so each user can only hold up to maxTokenLimit tokens from the TGE. PubKEY, amount if ETH is sent as internal values (msg.value if eth).
- registerUsersForSale() accepts an array of users with RegistrationNote pairs. Checks that registrationNotes are of the right format for each and adds each to Token’s whitelist using Token.whilteIstUser() for ech user or if too gas intensive, can call an equivalent whitelistUsers in the Token.
4. Sale Contract Highlights
- ReentrancyGuard (do we need this?) to secure purchase functions (possibly - review for necessity).
- Allows ETH and/or Stablecoin purchases.
- Whitelisting with off-chain signature (e.g. from our KYC service - Jumio is recommended). Registrar is referred to as KYC_Signer. The signature is referred to as registration key.
- Tracks or checks how many tokens a user already purchased/holds, so they cannot exceed their whitelisted limit.
- Sends proceeds to a secure deposit (multi-sig) address.
5. User Flow
- A user obtains a whitelisting signature (off-chain) from the project’s KYC/AML process.
- They call the buyWithETH or buyWithUSDC function, passing the signature.
- The contract verifies the signature, ensures they have not exceeded their limit, and transfers the purchased tokens from its pre-minted balance to the user.
6. Security
- The sale contract uses ReentrancyGuard to prevent malicious contract calls from re-entering purchase logic.
- Install OpenZeppelin: npm install @openzeppelin/contracts (and for upgradeable pattern: @openzeppelin/contracts-upgradeable).
- Compile with Solidity v0.8+.
Sales and Swap Contract SalesAndSwap.sol
// SPDX-License-Identifier: All code Copyright 2025 US Fintech LLC and Del Norte Holdings jointly. // OFFICIAL DEL NORTE NETWORK COMPONENT or pre-registration Del Norte membership token. // Provides immediate membership access to the DelNorte dev platform at different levels. // Required Non US or accredited US registration to swap to DTV token. // Registration available within 180 days per terms.delnorte.io . // swappableContracts added require isWhitelisted() method and expect a ReleaseManager to addSchedule(). // This component is minimally tested. Use at your own risk. // Designed by Ken Silverman as part of his ElasticTreasury (HUB and SPOKE), // PeerTreasury and Controller model. // This deployment is for Trueviewchain Inc. a Panama entity and Del Norte El Salvador S.A a subsidiary of Del Norte Holdings, Delaware USA. // Compilation help from Maleeha Naveed. Deployed by Maleeha Naveed on behalf of Del Norte. pragma solidity ^0.8.17;
/* Part 1: SALE Contract (Professional-Grade) ------------------------------------------ Key Features: - Pre-minted tokens held by this contract. - Whitelisting via ECDSA signature (off-chain KYC). - Reentrancy guard for secure buy functions. - Minimal gas usage for end-user purchases. */
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // ECDSA needed only to verify Registrar import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "./ElasticTreasuryHub.sol"; import "./ElasticTreasurySpoke.sol"; import "./Recovery.sol";
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
// Import base contracts (assumed to be located in the same directory) import "./ElasticTreasuryHub.sol"; import "./PeerTreasury.sol";
/* ========== INTERFACES ========== */
// Minimal ERC20 interface. interface IERC20 { function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); }
// ERC20 metadata interface to read symbol and decimals. interface IERC20Metadata is IERC20 { function symbol() external view returns (string memory); function decimals() external view returns (uint8); }
// Interface for the release manager that creates vesting schedules. // Here the release manager is assumed to internally know which token to release. interface IReleaseManager { function addSchedule( address recipient, uint256 startBlock, // The block number when vesting (cliff) starts. uint256 numberOfPayments, uint256 amountPerPayment ) external; }
// The controller verifies official entities (such as swappable tokens and treasury admins). interface IController { function isOfficialEntity(string calldata entityType, address entity) external view returns (bool); function addOfficialEntity(string memory, address, string memory, string memory) external returns (bool); }
// Interface for the swap-to token contract that includes a whitelist function. interface ISwapToToken { function isWhitelisted(address user) external view returns (bool); }
// Interface for a Chainlink price aggregator (e.g. ETH/USD). interface AggregatorV3Interface { function latestRoundData() external view returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ); }
/* ========== CONSTANTS ========== */ // Minimum purchase is $300 worth of tokens. For a fixed price of $0.05 per token, // this equals 6000 tokens (6000 * 1e18 in 18-decimal form) uint256 constant MIN_TOKEN_AMOUNT = 6000 * 1e18; // The vesting schedule is defined as 10 equal payments. uint256 constant NUMBER_OF_PAYMENTS = 10;
// USDC decimals retrieved in the constructor for gas efficiency. uint8 public usdcDecimals;
/* ========== MAIN CONTRACT ========== */
/** * @title TokenSaleAndSwap * @dev This contract implements both a token swap and token sale mechanism. * * The constructor accepts: * - _swapToTokenAddress: The address of the token that users ultimately receive. * (For example, think of this as a "DTV" token -- here referred to generically as the swap-to token.) * - _swapToTokenSymbol: The expected symbol of that token (used as a sanity check against address typos). * - _releaseManager: The release manager contract address that handles vesting schedules. * - _controller: The controller contract address (used for official entity checks). * This is now passed up to the ElasticTreasuryHub base contract. * - _usdcAddress: The USDC token contract address. * - _ethUsdPriceFeed: The Chainlink ETH/USD price feed address. * * The swapToSchedule() method allows a user to swap an official swappable token (passed as input) * for a vesting schedule of the swap-to token. * * The buy methods (buyWithUSDC and buyWithETH) let a whitelisted user purchase the swap-to token * at a price set in USD (default $0.05 per token). The USDC purchase method automatically scales amounts * (from USDC's 6 decimals up to 18). Both purchase methods require a minimum purchase equivalent to $300. * * Administrative methods: * - setSaleActive(bool): Only a TreasuryAdmin (verified via the controller) may change sale status. * - setPurchasePriceInUSD(uint256): Only a TreasuryAdmin may update the purchase price per token. * * IMPORTANT: For swapToSchedule() and buyWithUSDC(), the user must first call approve() on the respective pre-reg token * so that this contract is allowed to transfer the given amount.
* swap will reject if user is not whitelisted on the swappable token contract via isWhitelisted */ contract TokenSaleAndSwap is ElasticTreasuryHub, PeerTreasury { // Address of the token that users ultimately receive (swap-to token). address public immutable swapToTokenAddress; // Expected symbol of the swap-to token (used as a sanity check). string public immutable swapToTokenSymbol; // Address of the USDC token contract. address public immutable usdcAddress; // Release manager contract that handles vesting schedules. IReleaseManager public releaseManager; // Controller contract for verifying official entities. IController public controller; // Chainlink price feed for ETH/USD. AggregatorV3Interface public ethUsdPriceFeed;
// Sale status--if false, purchases are rejected. bool public saleActive; // Purchase price per token in USD (18 decimals); initially set to $0.05 per token. uint256 public purchasePriceInUSD;
// Overall total of tokens swapped or purchased (in swap-to token units, 18 decimals). uint256 public totalTokensSwapped; // Mapping to track totals per payment token: use usdcAddress for USDC, and address(0) for ETH. mapping(address => uint256) public tokensSwappedFrom;
/* ========== EVENTS ========== */
// Emitted when a user successfully schedules a token swap vesting schedule. event SwapScheduled( address indexed user, address indexed swappableToken, uint256 amount, uint256 startBlock, uint256 numberOfPayments, uint256 amountPerPayment );
// Emitted when a user successfully purchases tokens (via USDC or ETH). event TokenPurchased( address indexed user, address indexed paymentToken, // For USDC, use its token address; for ETH, use address(0) uint256 paymentAmount, uint256 tokenAmount, uint256 startBlock, uint256 numberOfPayments, uint256 amountPerPayment );
// Emitted when a token purchase fails (for example, due to lack of whitelist status). event TokenPurchaseFailure( address indexed user, string reason );
// Emitted when the sale status is changed (enabled or disabled). event SaleStatusChanged( bool newStatus );
// Emitted when the purchase price per token is updated. event PurchasePriceChanged( uint256 newPrice );
// Emitted when a new swappable token is added via the admin function. event SwappableTokenAdded( address indexed token );
// Emitted when a swappable token is removed via the admin function. event SwappableTokenRemoved( address indexed token );
/* ========== CONSTRUCTOR ========== */ /** * @dev Constructor. * @param _swapToTokenAddress The address of the token users receive. * @param _swapToTokenSymbol The expected symbol of the swap-to token. * @param _releaseManager The release manager contract address. * @param _controller The controller contract address. * This value is also passed to ElasticTreasuryHub. * @param _usdcAddress The USDC token contract address. * @param _ethUsdPriceFeed The Chainlink ETH/USD price feed address. */ constructor( address _swapToTokenAddress, string memory _swapToTokenSymbol, address _releaseManager, address _controller, address _usdcAddress, address _ethUsdPriceFeed ) ElasticTreasuryHub(_controller) // Pass the controller up to ElasticTreasuryHub. { // Sanity check: the token at _swapToTokenAddress should return the expected symbol. require( keccak256(bytes(IERC20Metadata(_swapToTokenAddress).symbol())) == keccak256(bytes(_swapToTokenSymbol)), "Incorrect swap-to token address provided; symbol mismatch" ); swapToTokenAddress = _swapToTokenAddress; swapToTokenSymbol = _swapToTokenSymbol; releaseManager = IReleaseManager(_releaseManager); controller = IController(_controller); usdcAddress = _usdcAddress; ethUsdPriceFeed = AggregatorV3Interface(_ethUsdPriceFeed); // Retrieve and store USDC decimals (e.g. should be 6 for USDC) to avoid extra external calls. usdcDecimals = IERC20Metadata(_usdcAddress).decimals(); // Set an initial purchase price of $0.05 per token (in 18 decimals). purchasePriceInUSD = 5e16; // 0.05 * 1e18 // Initially, sale is inactive. saleActive = false;
}
/* ========== ADMIN FUNCTIONS ========== */ /** * @notice Enable or disable the token sale. * @param _active True to enable the sale, false to disable. * * Requirements: * - Caller must be an official TreasuryAdmin. */ function setSaleActive(bool _active) external { require( controller.isOfficialEntity("TreasuryAdmin", msg.sender), "Only TreasuryAdmin can change sale status" ); saleActive = _active; emit SaleStatusChanged(_active); }
/** * @notice Set the purchase price per token in USD (18 decimals). * @param newPrice The new price per token (e.g., 5e16 for $0.05). * * Requirements: * - Caller must be an official TreasuryAdmin. */ function setPurchasePriceInUSD(uint256 newPrice) external { require( controller.isOfficialEntity("TreasuryAdmin", msg.sender), "Only TreasuryAdmin can change purchase price" ); purchasePriceInUSD = newPrice; emit PurchasePriceChanged(newPrice); }
/* ========== CORE FUNCTIONS ========== */
/** * @notice Swap an approved swappable token for a vesting schedule of the swap-to token. * @param swappableToken The address of the token being swapped (must be an official swappable token). * @param amount The amount of swappable token to swap. * * Requirements: * - The sale must be active. * - The caller must be whitelisted by the swap-to token contract. * - The provided token must be officially recognized as swappable. * - The user must have pre-approved this contract to transfer their tokens. * - Vesting schedule: 10 equal payments starting ~90 days (block numbers) in the future. */ function swapToSchedule(address swappableToken, uint256 amount) external { require(saleActive, "Token sale is not active");
// Verify that the caller is whitelisted via the swap-to token contract. require( ISwapToToken(swapToTokenAddress).isWhitelisted(msg.sender), "Swap failed: caller is not whitelisted" );
// Verify that the swappable token is officially recognized. require( controller.isOfficialEntity("SwappableToken", swappableToken), "Swap failed: token is not officially recognized as swappable" );
// Transfer the swappable tokens from the user to this contract. // NOTE: The user must first call approve() on the token contract. require( IERC20(swappableToken).transferFrom(msg.sender, address(this), amount), "Swap failed: token transfer failed -- ensure you have approved the contract" );
// Set up vesting schedule: 10 equal payments starting ~90 days in the future. uint256 amountPerPayment = amount / NUMBER_OF_PAYMENTS; uint256 startBlock = block.number + (90 * 6500); // Approximate block number for 90 days ahead.
// Create the vesting schedule via the release manager. releaseManager.addSchedule(msg.sender, startBlock, NUMBER_OF_PAYMENTS, amountPerPayment);
// Update tracking. totalTokensSwapped += amount; tokensSwappedFrom[swappableToken] += amount;
emit SwapScheduled(msg.sender, swappableToken, amount, startBlock, NUMBER_OF_PAYMENTS, amountPerPayment); }
/** * @notice Purchase the swap-to token by paying with USDC. * @param usdcAmount The amount of USDC the user is paying. * * Requirements: * - The sale must be active. * - The caller must be whitelisted by the swap-to token contract. * - The USDC amount (converted to 18 decimals) must be at least $300. * - The user must have approved this contract to spend USDC. * * The token amount purchased is calculated based on the purchase price (in USD, 18 decimals), * and a vesting schedule is set for 10 equal payments starting ~90 days in the future. */ function buyWithUSDC(uint256 usdcAmount) external { require(saleActive, "Token sale is not active"); if (!ISwapToToken(swapToTokenAddress).isWhitelisted(msg.sender)) { emit TokenPurchaseFailure(msg.sender, "Caller is not whitelisted"); revert("Caller is not whitelisted"); } // Use the stored usdcDecimals value to convert USDC amounts to 18-decimal format. uint256 usdcAmount18 = usdcAmount; if (usdcDecimals < 18) { usdcAmount18 = usdcAmount * (10 ** (18 - usdcDecimals)); } else if (usdcDecimals > 18) { usdcAmount18 = usdcAmount / (10 ** (usdcDecimals - 18)); } require(usdcAmount18 >= 300e18, "Minimum purchase is $300 in USDC"); // Calculate token amount purchased at the current purchase price. // tokenAmount = (usdcAmount18 * 1e18) / purchasePriceInUSD. uint256 tokenAmount = (usdcAmount18 * 1e18) / purchasePriceInUSD; // Transfer USDC from the user to this contract. require( IERC20(usdcAddress).transferFrom(msg.sender, address(this), usdcAmount), "USDC transfer failed -- ensure you have approved the contract" ); uint256 amountPerPayment = tokenAmount / NUMBER_OF_PAYMENTS; uint256 startBlock = block.number + (90 * 6500); // Create the vesting schedule. releaseManager.addSchedule(msg.sender, startBlock, NUMBER_OF_PAYMENTS, amountPerPayment); totalTokensSwapped += tokenAmount; tokensSwappedFrom[usdcAddress] += tokenAmount; emit TokenPurchased(msg.sender, usdcAddress, usdcAmount, tokenAmount, startBlock, NUMBER_OF_PAYMENTS, amountPerPayment); }
/** * @notice Purchase the swap-to token by paying with ETH. * * The ETH amount sent is converted to USD using Chainlink. * The USD value must be at least $300. * The token amount purchased is determined based on the current purchase price. * A vesting schedule is set for 10 equal payments starting ~90 days in the future. */ function buyWithETH() external payable { require(saleActive, "Token sale is not active"); if (!ISwapToToken(swapToTokenAddress).isWhitelisted(msg.sender)) { emit TokenPurchaseFailure(msg.sender, "Caller is not whitelisted"); revert("Caller is not whitelisted"); } (, int256 price, , ,) = ethUsdPriceFeed.latestRoundData(); require(price > 0, "Invalid price from oracle"); // Convert ETH (msg.value) to USD (18 decimals): // Chainlink price has 8 decimals; multiply by 1e10. uint256 usdValue = (msg.value * uint256(price) * 1e10) / 1e18; require(usdValue >= 300e18, "Minimum purchase is $300 in ETH equivalent"); uint256 tokenAmount = (usdValue * 1e18) / purchasePriceInUSD; uint256 amountPerPayment = tokenAmount / NUMBER_OF_PAYMENTS; uint256 startBlock = block.number + (90 * 6500); // Create the vesting schedule. releaseManager.addSchedule(msg.sender, startBlock, NUMBER_OF_PAYMENTS, amountPerPayment); totalTokensSwapped += tokenAmount; tokensSwappedFrom[address(0)] += tokenAmount; emit TokenPurchased(msg.sender, address(0), msg.value, tokenAmount, startBlock, NUMBER_OF_PAYMENTS, amountPerPayment); }
function addSwappableToken(address token) external { // should fail if the sender is not a TreasuryAdmin require(super.isContract(token),"token must be a contract address"); IController(controller).addOfficialEntity("SwappableToken",token,"A PreRegToken",""); emit SwappableTokenAdded(token); } function removeSwappableToken(address token) external { require(super.isContract(token),"token must be a contract address"); // should fail if the sender is not a TreasuryAdmin IController(controller).removeOfficialEntity("SwappableToken",token,"preRegToken",); emit SwappableTokenRemoved(token); } }
|
Scanner function support for state viewing via web3, TODO
Key Points & Comments
- Owner can set or update the token price.
- Signature logic allows an off-chain whitelisting service to generate a signature for address + tokenLimit.
- The contract checks how many tokens a user already holds rather than tracking cumulative buys.
- Min investment in ETH or USDC is enforced (e.g., 0.1 ETH or $400 USDC).
- Actual production code often uses Chainlink or another oracle for ETH price. Here, we allow manual setting to save GAS (PURCHASERS must be notified that the number of tokens per ETH will be fixed at time of sale and periodically updated unless we add the oracle lookup).
- The user’s maximum token limit is in the registration signature (e.g., 25,000 tokens).
- The deposit address is a multi-sig known only to the CEO/COO. “Do not deposit directly to it.” Make it clear on website do NOT attempt any direct deposit to deposit address.
PeerTreasury PeerTreasury.sol
allows for controlled movement of USDC between Sales and Vesting contract.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "./IController.sol";
// New! Tracked Peer Treasury transfers between OfficialEntity Smart Contracts for ETH and ERC20 tokens contract PeerTreasury { event PeerTreasuryReceivedETH(address indexed sender, uint256 amount); event PeerTreasuryReceivedTokens(address indexed sender, address indexed token, uint256 amount); event PeerTreasuryTransferredETH(address indexed to, uint256 amount); event PeerTreasuryTransferredTokens(address indexed to, address indexed token, uint256 amount);
struct TreasuryEntry { uint256 totalReceived; uint256 totalWithdrawn; }
address public controller; mapping(address => TreasuryEntry) public tokenPeerTreasury; TreasuryEntry public ethPeerTreasury;
modifier onlyTreasuryAdmin() { require( IController(controller).isOfficialDoubleEntity("TreasuryAdmin", msg.sender, "SmartContract", msg.sender, false), "Unauthorized" ); _; }
constructor(address _controller) { controller = _controller; }
/// @notice Receive ETH from any source and track it function peerTreasuryReceiveETH() external payable { require(msg.value > 0, "No ETH sent"); ethPeerTreasury.totalReceived += msg.value; emit PeerTreasuryReceivedETH(msg.sender, msg.value); }
/// @notice Transfer ETH to another peer contract (OfficialEntity SmartContract) function peerTreasuryTransferETH(address payable to, uint256 amount) external onlyTreasuryAdmin { require(address(this).balance >= amount, "Insufficient contract ETH balance"); require(controller.isOfficialEntity("SmartContract", to), "Recipient not an official SmartContract");
ethPeerTreasury.totalWithdrawn += amount; PeerTreasury(to).peerTreasuryReceiveETH{value: amount}(); emit PeerTreasuryTransferredETH(to, amount); }
/// @notice Receive ERC20 tokens and track source function peerTreasuryReceiveTokens(address token, uint256 amount) external { require(amount > 0, "Zero amount"); require(IERC20(token).transferFrom(msg.sender, address(this), amount), "Transfer failed"); tokenPeerTreasury[token].totalReceived += amount; emit PeerTreasuryReceivedTokens(msg.sender, token, amount); }
/// @notice Transfer ERC20 tokens to another peer contract (OfficialEntity SmartContract) function peerTreasuryTransferTokens(address token, address to, uint256 amount) external onlyTreasuryAdmin { require(IERC20(token).balanceOf(address(this)) >= amount, "Insufficient token balance"); require(controller.isOfficialEntity("SmartContract", to), "Recipient not an official SmartContract");
tokenPeerTreasury[token].totalWithdrawn += amount; require(IERC20(token).approve(to, amount), "Approve failed"); PeerTreasury(to).peerTreasuryReceiveTokens(token, amount); emit PeerTreasuryTransferredTokens(to, token, amount); }
/// @notice Check ETH available (total received - total withdrawn) function peerETHAvailable() external view returns (uint256) { return ethPeerTreasury.totalReceived - ethPeerTreasury.totalWithdrawn; }
/// @notice Check token available (total received - total withdrawn) function peerTokenAvailable(address token) external view returns (uint256) { TreasuryEntry storage e = tokenPeerTreasury[token]; return e.totalReceived - e.totalWithdrawn; } } // End PeerTreasury |
2.3 VESTING (redesigned and reviewed, 4/9/2025)
Vesting Contract Design Document (Elastic Treasury Compatible)
1. Overview
The Vesting Contract securely handles scheduled token releases (vesting) for any token type and ETH. It is a SPOKE for receiving tokens from token contract and also allows receiving USDC as an External Treasury first being withdrawn from the salesContract (also an ExternalTreasury) .
2. Data Structures
Schedule Structure
Each claimant may have just ONE schedule, that in some cases can be merged to.
2.1 Hub-and-Spoke Integration
Elastic Treasury Spoke (of Token for DTV and Sales for USDC (cannot have two hubs yet, so USDC will get into vesting by ExternalTreasury calls, sales and vesting need both extend ExternalTreasury) | +-------------------+ | Vesting Contract | |-------------------| | treasuryReceive() | | treasuryReclaim() | +-------------------+ |
Vesting Contract ReleaseScheduleManager.sol
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "./IController.sol"; import "./IElasticTreasuryHub.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "./ElasticTreasurySpoke.sol"; import "./ExternalTreasury.sol"; // New! External Treasury for tracked ETH/token inflows/outflows
contract ReleaseScheduleManager is ElasticTreasurySpoke, PeerTreasury { struct Schedule { uint256 singlePayAmount; uint256 numPayments; uint256 totalPayments; uint256 totalValue; uint256 totalLeftToPay; uint256 startBlockNum; uint256 lastClaimedBlock; uint256 totalPaidOut; bool isRevokable; }
struct BeneficiarySchedule { address beneficiary; Schedule schedule; }
mapping(address => mapping(address => Schedule)) public schedules; mapping(address => Schedule) public ethSchedules; mapping(address => address[]) private tokenScheduleBeneficiaries; uint256 public globalAmountOwed;
constructor(address _controller, address _hub) ElasticTreasurySpoke(_controller, _hub) ExternalTreasury(_controller) {}
modifier onlyTreasuryAdminOrContract() { require( IController(controller).isOfficialDoubleEntity("TreasuryAdmin", msg.sender, "SmartContract", msg.sender, false), "Unauthorized caller" ); _; }
/// @notice Add a new vesting schedule or merge/overwrite an existing one depending on flags function addReleaseSchedule( address tokenContractAddress, address beneficiary, uint256 singlePayAmount, uint256 numPayments, uint256 totalPayments, uint256 startBlockOffset, bool isRevokable, bool isMerge, bool ignoreRevokableCheck ) public onlyTreasuryAdminOrContract returns (bool added) { Schedule storage existing = schedules[tokenContractAddress][beneficiary]; uint256 newTotal = singlePayAmount * numPayments; uint256 startBlock = block.number + startBlockOffset;
if (existing.totalValue > 0) { if (isMerge) { require(existing.startBlockNum == startBlock, "Start block mismatch on merge"); require(existing.isRevokable, "Cannot merge into non-revokable schedule"); globalAmountOwed = globalAmountOwed + newTotal - existing.totalValue;
existing.singlePayAmount += singlePayAmount; existing.numPayments += numPayments; existing.totalPayments = totalPayments; existing.totalValue += newTotal; existing.totalLeftToPay += newTotal; existing.isRevokable = existing.isRevokable && isRevokable; added = true; } else { require(existing.isRevokable || ignoreRevokableCheck, "Schedule already exists and is not revokable"); globalAmountOwed = globalAmountOwed + newTotal - existing.totalValue;
schedules[tokenContractAddress][beneficiary] = Schedule({ singlePayAmount: singlePayAmount, numPayments: numPayments, totalPayments: totalPayments, totalValue: newTotal, totalLeftToPay: newTotal, startBlockNum: startBlock, lastClaimedBlock: 0, totalPaidOut: 0, isRevokable: isRevokable }); added = true; } } else { require(IERC20(tokenContractAddress).balanceOf(address(this)) >= newTotal, "Insufficient tokens for new schedule"); schedules[tokenContractAddress][beneficiary] = Schedule({ singlePayAmount: singlePayAmount, numPayments: numPayments, totalPayments: totalPayments, totalValue: newTotal, totalLeftToPay: newTotal, startBlockNum: startBlock, lastClaimedBlock: 0, totalPaidOut: 0, isRevokable: isRevokable }); tokenScheduleBeneficiaries[tokenContractAddress].push(beneficiary); globalAmountOwed += newTotal; added = true; } }
/// @notice Batch add schedules using a flattened 6-tuple array function addReleaseScheduleList( address tokenContractAddress, address[6][] calldata scheduleData ) external onlyTreasuryAdminOrContract returns (uint256 addedCount) { for (uint i = 0; i < scheduleData.length; i++) { (address b, uint256 s, uint256 n, uint256 t, uint256 o, uint256 r) = ( scheduleData[i][0], scheduleData[i][1], scheduleData[i][2], scheduleData[i][3], scheduleData[i][4], scheduleData[i][5] ); if ( addReleaseSchedule(tokenContractAddress, b, s, n, t, o, r > 0, false, false) ) { addedCount++; } } }
/// @notice Claims all available payments up to the current block for the caller function claim(address tokenContractAddress) external { Schedule storage sch = schedules[tokenContractAddress][msg.sender]; require(block.number >= sch.startBlockNum, "Not started yet");
uint256 paymentsEligible = (block.number - sch.startBlockNum); uint256 totalEligible = paymentsEligible * sch.singlePayAmount; uint256 amountToPay = totalEligible - sch.totalPaidOut;
require(amountToPay > 0 && sch.totalLeftToPay >= amountToPay, "Nothing to claim");
sch.totalPaidOut += amountToPay; sch.totalLeftToPay -= amountToPay; sch.lastClaimedBlock = block.number; globalAmountOwed -= amountToPay;
require(IERC20(tokenContractAddress).transfer(msg.sender, amountToPay), "Transfer failed"); }
/// @notice Removes a single revokable schedule function removeSchedule(address tokenContractAddress, address beneficiary) external onlyTreasuryAdminOrContract returns (bool removed) { Schedule storage s = schedules[tokenContractAddress][beneficiary]; if (s.isRevokable) { globalAmountOwed -= s.totalLeftToPay; delete schedules[tokenContractAddress][beneficiary]; removed = true; } }
/// @notice Removes multiple revokable schedules in one call function removeMultipleSchedules(address tokenContractAddress, address[] calldata beneficiaries) external onlyTreasuryAdminOrContract returns (uint256 removedCount) { for (uint i = 0; i < beneficiaries.length; i++) { if (removeSchedule(tokenContractAddress, beneficiaries[i])) { removedCount++; } } }
/// @notice Returns all schedules for a token type function getAllSchedulesByToken(address tokenContractAddress) external view returns (BeneficiarySchedule[] memory) { address[] memory benes = tokenScheduleBeneficiaries[tokenContractAddress]; BeneficiarySchedule[] memory results = new BeneficiarySchedule[](benes.length); for (uint i = 0; i < benes.length; i++) { results[i] = BeneficiarySchedule({ beneficiary: benes[i], schedule: schedules[tokenContractAddress][benes[i]] }); } return results; }
/// @notice Returns a single schedule if it exists function getSchedule(address tokenContractAddress, address beneficiary) external view returns (BeneficiarySchedule memory) { Schedule memory sch = schedules[tokenContractAddress][beneficiary]; require(sch.totalValue > 0, "Schedule does not exist"); return BeneficiarySchedule({ beneficiary: beneficiary, schedule: sch }); }
// CHANGED: removed receive() fallback — ETH funding now handled by ExternalTreasury }
|
Comprehensive Design: Liquidity Pool
LP standard Uniswap factory contracts, token remains gated so DEX operations must be performed through.
Challenges, require the LP to except only registered users to trade and allow the Membership contract exclusive rights to bypass giant for purchase of tokens for users. Table of Contents (two smart contracts deployed by the Uniswap factory method)
Token contract transfer method itself is restricted in the token so tradeable on any DEX if the user (both seller and buyer) are whitelisted. In other words, the dex will likely fail unless buyer and seller come together via the website and are both registered. The main purpose of the DEX pool is for the Membership Contract to purchase tokens (which is not gated)
Concept Overview
Purpose of the Liquidity Pool Contracts
Key Features
Gated Access Design
System Design and Components
USDC Service Fee Reserve Pool
Liquidity Pool Contracts (USDC and DTVT)
Gating Mechanism with Registration Key
Workflow Description
Flow of Funds from Reserve Pool to Liquidity Pool
User Interaction via Registration Key
Smart Contract Design
Key Variables and Structures
Functions and Logic
Code Implementation
USDC Service Fee Reserve Pool Contract
Liquidity Pool Contract with Gated Access
Considerations and Future Enhancements
Security
Scalability
Interoperability
1. Concept Overview
Purpose of the Liquidity Pool Contracts
The liquidity pool contracts serve two primary purposes:
To manage funds collected as USDC service fees and use these funds to maintain liquidity for the DTVT token.
To create a secure and gated liquidity pool for the DTVT token, ensuring only authorized participants can interact with it.
Key Features
USDC Service Fee Reserve Pool:
Collects and holds USDC service fees.
Automatically moves funds to the DTVT liquidity pool when a predefined threshold is reached.
Liquidity Pool Contracts (USDC and DTVT):
Operate as an Automated Market Maker (AMM) but with gated access.
Allow interactions only from the Reserve Pool contract or users with verified registration keys.
Ensures security by requiring a signed registration key from a trusted registrar for user interactions.
Limits unauthorized access and tampering with liquidity pool funds.
Gated Access Design
The gating mechanism involves:
A trusted registrar with a hardcoded public key.
Verification of a user’s signed message, ensuring their public key matches the registration key.
Interaction restrictions, allowing only:
Funds transferred from the USDC Service Fee Reserve Pool.
Registered users with valid credentials.
2. System Design and Components
USDC Service Fee Reserve Pool
The reserve pool contract collects USDC service fees and monitors the balance. When the balance exceeds the ACTION_THRESHOLD_AMOUNT (default: $10,000), it:
Automatically transfers $10,000 worth of USDC to the DTVT liquidity pool.
Ensures consistent liquidity for DTVT token trading.
Liquidity Pool Contracts (USDC and DTVT)
Two liquidity pools are maintained:
LIQUIDITY_POOL_USDC: Holds USDC for the AMM.
LIQUIDITY_POOL_DTVT: Holds DTVT tokens for the AMM.
Both pools operate like a Uniswap-style AMM but with additional restrictions:
Gated access requiring a valid registration key or interaction from the Reserve Pool contract.
Custom pricing logic based on the AMM formula.
Gating Mechanism with Registration Key
The gating mechanism ensures only authorized interactions by:
Requiring a signed message from users.
Verifying the signature using a hardcoded public key of the trusted registrar.
Allowing interactions only if the message and public key match.
3. Workflow Description
Flow of Funds from Reserve Pool to Liquidity Pool
USDC Service Fee Collection:
USDC fees are collected and stored in the USDC_SERVICE_FEE_RESERVE_POOL.
Threshold Monitoring:
The contract checks if the balance exceeds ACTION_THRESHOLD_AMOUNT.
Liquidity Pool Funding:
If the threshold is reached, $10,000 worth of USDC is transferred to the LIQUIDITY_POOL_USDC.
The corresponding amount of DTVT tokens is purchased from LIQUIDITY_POOL_DTVT based on the AMM formula.
User Interaction via Registration Key
Registration:
A user obtains a signed registration key from the trusted registrar.
Signed Message Verification:
The user’s interaction request includes their public key and a signed message.
The contract verifies the signature against the registrar’s hardcoded public key.
Interaction:
Upon successful verification, the user can interact with the liquidity pool to trade USDC and DTVT tokens.
4. Smart Contract Design
Key Variables and Structures
USDC Service Fee Reserve Pool Contract
actionThresholdAmount: Minimum balance required to trigger fund transfer.
reserveBalance: Tracks the current USDC balance.
liquidityPoolAddress: Address of the DTVT liquidity pool contract.
Liquidity Pool Contract
LIQUIDITY_POOL_USDC: Holds USDC.
LIQUIDITY_POOL_DTVT: Holds DTVT tokens.
trustedRegistrarKey: Hardcoded public key of the trusted registrar.
registeredUsers: Mapping of verified user public keys.
ammReserves: Tracks USDC and DTVT token balances.
Functions and Logic
USDC Service Fee Reserve Pool Contract
checkAndTransferFunds():
Checks if the balance exceeds the threshold.
Transfers $10,000 USDC to the DTVT liquidity pool.
depositUSDC(uint256 amount):
Allows deposits into the reserve pool.
Liquidity Pool Contract
Challenge : provide failure without taking in funds to pool in the case of unregistered buyer (seller is easy, sells in to pool in allo cases. If they have DTV they are registered and either NON-US or registered users with no crossover! Any US holder we know to have purchased over 12 months ago.):
Verifies the user’s registration key.
swapUSDCtoDTVT(uint256 usdcAmount):
Executes a token swap based on the AMM formula.
Requires verification of user registration.
swapDTVTtoUSDC(uint256 dtvtAmount):
Executes a reverse swap.
Requires verification of user registration.
5. Code Implementation
USDC Service Fee Reserve Pool Contract
In DEVELOPMENT -
function recoverSigner(bytes memory message, bytes memory signature) internal pure returns (address) {
// ECDSA signature recovery logic
}
}
6. Considerations and Future Enhancements
Security:
Harden signature verification.
Implement rate limits to prevent abuse.
Scalability:
Optimize gas usage for AMM operations.
Interoperability:
Integrate with external DeFi protocols for enhanced liquidity options.
This design ensures a secure and efficient liquidity pool system with controlled access, facilitating robust management of USDC and DTVT token liquidity.
2.4 Staking Contract with Reward Tranches - Design
(Note this contract will not be implemented in phase 1 until clearer SEC regulation is written confirming our protections are adequate for our utility token to serve as an equity token in the future.)
1. Introduction
This design introduces a staking contract with a novel reward mechanism based on fixed reward tranches that must be EARNED to be claimed by performing property owner validation steps. Unlike traditional staking contracts, this design precomputes rewards per token for each tranche (referred to as Reward Accounts) when a predefined treasury threshold is met. That award then must be earned by performing an increasing number of validations to provide property owner badges based on the amount in the pool there is to earn for the given staker. This approach offers several benefits, including:
Common Reward Mechanisms in Staking
-
Proportional Reward Distribution:
-
Rewards are distributed proportionally based on the number of tokens staked.
- Stakers must lock their tokens to participate in the reward pool.
-
- Dynamic Reward Calculations:
-
Rewards are calculated dynamically based on the time and amount of tokens staked.
-
- Claim and Unstake Restrictions:
-
Users must claim rewards before they can unstake tokens to prevent unclaimed rewards from being lost.
Novel Strategies in This Design
-
Reward Tranches (Reward Accounts):
-
Rewards are grouped into fixed tranches of $100,000 (or a configurable value) whenever the treasury accrues sufficient funds.
- Each tranche stores:
-
A snapshot of the total staked tokens at the time of creation.
- A precomputed reward per token, ensuring efficient claim calculations.
-
- Immutable Tranches:
-
Once a tranche is created, it remains unchanged, even as new staking or unstaking operations occur. This eliminates recalculations and ensures consistency.
-
- Efficient Claim Logic:
-
Rewards are calculated only for tranches created since the user's last claim, reducing gas usage.
-
- Simplified Staking/Unstaking:
-
Staking and unstaking operations do not retroactively affect rewards for existing tranches, simplifying logic and reducing gas costs.
Advantages
-
Claims iterate only over new tranches, and rewards are precomputed at tranche creation.
-
Rewards are tied to the user's stake at the time of tranche creation.
-
The design scales well, even with frequent staking/unstaking operations and large numbers of tranches.
-
Immutable tranches ensure that all reward calculations are deterministic and auditable.
2. Contract Overview
The smart contract is structured as follows:
-
Core Data Structures:
-
Reward Accounts (tranches) store reward information for each $100,000 deposit.
- User data tracks the staked amount, last claim block, and other relevant metrics.
-
- Core Functions:
-
Deposit: Adds funds to the treasury and creates new reward tranches.
- Stake: Allows users to lock tokens into the staking pool.
- Claim: Calculates and distributes rewards for all tranches since the user's last claim.
- Unstake: Allows users to withdraw their staked tokens, provided rewards have been claimed.
-
- Optimizations:
-
Precomputed rewardPerToken values minimize computational overhead.
- Immutable reward accounts ensure gas-efficient claims.
3. Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function totalSupply() external view returns (uint256);
}
contract StakingRewards {
IERC20 public token; // The token being staked
IERC20 public USDC; // Reward token (e.g., USDC)
uint256 public totalStakedTokens;
uint256 public treasuryBalance;
uint256 public unaccountedAccruedBalance;
uint256 public rewardAccountID;
struct RewardAccountRecord {
uint256 rewardAccountID;
uint256 rewardAccountBalance;
uint256 rewardAccountBlockNum;
uint256 TotalTokensStaked;
uint256 rewardPerToken;
}
mapping(uint256 => RewardAccountRecord) public rewardAccounts;
mapping(address => uint256) public amountStaked;
mapping(address => uint256) public lastClaimBlockNum;
constructor(IERC20 _token, IERC20 _USDC) {
token = _token;
USDC = _USDC;
}
// Events for better tracking
event Staked(address indexed user, uint256 amount);
event Unstaked(address indexed user, uint256 amount);
event RewardsClaimed(address indexed user, uint256 amount);
}
4. Function Descriptions
4.1 Deposit
-
Adds funds to the treasury.
- Creates new reward accounts (tranches) for every $100,000 in unaccounted balance.
-
amount: The amount of USDC deposited into the treasury.
-
Updates treasuryBalance and unaccountedAccruedBalance.
- Creates reward accounts until the unaccounted balance is less than $100,000.
function deposit(uint256 amount) public onlyOwner {
require(amount > 0, "Deposit must be greater than zero");
treasuryBalance += amount;
unaccountedAccruedBalance += amount;
while (unaccountedAccruedBalance >= 100_000 * 1e6) {
_createRewardAccount();
}
}
function _createRewardAccount() internal {
uint256 rewardBalance = 100_000 * 1e6;
unaccountedAccruedBalance -= rewardBalance;
rewardAccountID++;
uint256 rewardPerToken = (rewardBalance * 1e18) / totalStakedTokens;
rewardAccounts[rewardAccountID] = RewardAccountRecord({
rewardAccountID: rewardAccountID,
rewardAccountBalance: rewardBalance,
rewardAccountBlockNum: block.number,
TotalTokensStaked: totalStakedTokens,
rewardPerToken: rewardPerToken
});
}
4.2 Stake
-
Locks user tokens into the staking pool.
- Initializes lastClaimBlockNum for first-time stakers.
-
amount: The number of tokens to stake.
-
Transfers tokens to the contract.
- Updates amountStaked and totalStakedTokens.
function stake(uint256 amount) public {
require(amount > 0, "Stake amount must be greater than zero");
token.transferFrom(msg.sender, address(this), amount);
if (amountStaked[msg.sender] == 0) {
lastClaimBlockNum[msg.sender] = block.number;
}
amountStaked[msg.sender] += amount;
totalStakedTokens += amount;
emit Staked(msg.sender, amount);
}
4.3 Claim
-
Distributes rewards for all reward accounts created since the user's last claim.
-
Loops through eligible reward accounts.
- Calculates rewards based on rewardPerToken.
function claimRewards() public {
require(amountStaked[msg.sender] > 0, "No tokens staked");
uint256 rewards = 0;
uint256 startBlock = lastClaimBlockNum[msg.sender];
for (uint256 i = 1; i <= rewardAccountID; i++) {
RewardAccountRecord storage record = rewardAccounts[i];
if (record.rewardAccountBlockNum > startBlock) {
rewards += (amountStaked[msg.sender] * record.rewardPerToken) / 1e18;
}
}
lastClaimBlockNum[msg.sender] = block.number;
require(rewards > 0, "No rewards available");
USDC.transfer(msg.sender, rewards);
emit RewardsClaimed(msg.sender, rewards);
}
4.4 Unstake
-
Allows users to withdraw their staked tokens after claiming rewards.
-
Claims pending rewards.
- Updates amountStaked and totalStakedTokens.
solidity
Copy code
function unstake(uint256 amount) public {
require(amount > 0, "Unstake amount must be greater than zero");
require(amountStaked[msg.sender] >= amount, "Insufficient staked balance");
claimRewards();
amountStaked[msg.sender] -= amount;
totalStakedTokens -= amount;
token.transfer(msg.sender, amount);
emit Unstaked(msg.sender, amount);
}
5. Summary
This contract efficiently manages staking rewards with:
-
Immutable reward accounts for deterministic reward calculations.
- Precomputed rewardPerToken values for gas optimization.
- Simplified logic for staking, claiming, and unstaking operations.
Whitelisting Service “Callback” to Generate the Signature
Our off-chain backend:
-
A user completes KYC with the registrar.
- Registrar (which is connected to the owner’s wallet in MetaMask or via a server-based private key) signs (userAddress, maxTokenLimit).
- The user obtains the signature from your server.
- Your contract never needs to store the whitelisted addresses—it just verifies the signature at purchase time.
Conclusion
This four-contract architecture (Token, Sale, Release, Rewards) separates responsibilities:
- Token: Basic ERC20 with a cap.
- Sale: Handles TGE logic, price, whitelisting, and deposit routing.
- ReleaseManager: Sets up time-based release schedules for certain addresses. Claim fails if not registered.
- Rewards: Implements a custom mechanism for distributing treasury assets to token holders based on EARNED PROCESSING to VALIDATE PROPERTY OWNERS and therefore then based on a proportion of supply and earned rewards in pool.
National Property Manager and Registration System
The Municipal Property Management Registration System (MPMRS) is a hybrid framework designed to mint properties as ERC-721 compliant tokens and manage their metadata. This system integrates both centralized and decentralized components to ensure efficient, secure, and verifiable property management.
Components of the MRS:
- Full-stack centralized bridge between IPFS and blockchain NationalPropertyManager
- Scans and stores property data using cloud infrastructure, such as Google Cloud.
- MRS Smart Contract (MRS_SC):
- Municipal wallet:
Facilitates property token management for municipalities, including minting, districtEmployee updates, and ownership tracking.
Key Processes in the MRS
1. Metadata Creation
- The metadata structure now follows a flat model with three trail structures. Metadata is added to but never deleted, CIDs are used only for documents or images. All of which are prefaced with a title, action and action notes stored on chain
- Process Overview:
- When a record exists for a given propertyId, it will either have a minted 721 or not. If so, the on chain data can be synced with the IPFS data for verification.
- If no record exists: IPFS docs are written to the cluster then a new metadata structure is created, initializing key attributes like all docs and other details.
- Name/Address and property detail are both stored as trails. A trail[] has a CID for an Owner STRUCT JSON and a CID for a Property struct JSON - wherever modified, a new CID is added to the trail for that section.
2. Flat Metadata Structure
The flat structure stores CIDs for images, documents, and records while preserving the ability to update or append entries.
Three trail Structures for Each NFT:
- DocsAndImagesArr: [{ title, CID, action, notes, value}, ... ]
- ownerDetail STRUCT trail CIDs
- property STRUCT trail of CIDs: [ { CID: string, timestamp: uint256 }, ... ]
- Historical records are preserved for security and dispute resolution. Title and version of a doc is used for lookup of historical actions.
Minting a New Property
- The municipal wallet calls the mintProperty function:
- Parameters include the tokenId, propertyId, owner address, and metadata.
- Metadata contains initial values for, and recordCIDs.
- The mintProperty function:
- Mints a new ERC-721 token.
- Stores the propertyId in the tokenIdToPropertyId mapping.
- Initializes the metadata in the metadataMap.
Subdivision and Merging
- Subdivision: Splits a property into multiple new tokenIds, each mapped to a new propertyId.
- Updates the subdivisionMap for the original tokenId.
- Marks the original property as Subdivided.
- Merge: Combines multiple properties into a new tokenId and propertyId.
- Updates the mergeMap for each original tokenId.
- Marks the original properties as Merged.
- Only relevant metadata (e.g., the latest record) is actively used for operations.
- Preservation of History
4. Municipal Registry Smart Contract (MRS_SC) ERC-721
4.1 MRS_SC Structure
1. Token ID Map
- tokenIdMap: mapping(uint256 => address)
Maps a tokenId (NFT ID) to the public address of the registrant or current owner.
2. CID Metadata Map
- metadataMap: mapping(uint256 => Metadata)
Maps a tokenId to its corresponding metadata structure.
3. Metadata Structure
Each Metadata contains:
Variable Name | Description |
Docs STRUCT | [ { title: string, CID: string }, ... ] |
Owner trail STRUCT | [ { title: string, CID: string }, ... ] |
Prop trail STRUCT | [ { title: string, CID: string }, ... ] |
propTaxRecordsArr | [CID1, CID2, ..., CIDn] |
propPhysDetailRecordArr | [CID1, CID2, ..., CIDn] |
propLocationRecord | CID (latest, history preserved) |
propOwnerRecord | CID (latest, history preserved) |
propSubdivisionRecord | [CID1, CID2, ..., CIDn] |
propMergeRecord | [CID1, CID2, ..., CIDn] |
4. Subdivision and Merge Tracking
Variable Name | Description |
subdivisionMap | mapping(uint256 => uint256[]) |
Maps a tokenId to an array of tokenIds representing subdivisions. |
|
mergeMap | mapping(uint256 => uint256) |
Maps a tokenId to a single tokenId representing the larger merged parcel. |
|
5. State Tracking TBD
National Property Manager NationalPropertyManager.sol
// SPDX-License-Identifier: UNLICENSED // Copyright 2025 US Fintech LLC and DelNorte Holdings. // // Permission to use, copy, modify, or distribute this software is strictly prohibited // without prior written consent from both copyright holders. // // 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 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. // OFFICIAL DEL NORTE NETWORK COMPONENT // Provides immediate membership access to platform at different levels. // Required Non US or accredited US registration to swap for DTV token. Registration available within 180 days per terms.delnorte.io . // Minimally tesed Conroller Tree for world-wide government administration of, well, anything, including property ownership. // Designed by Ken Silverman as part of his ElasticTreasury (HUB and SPOKE), PeerTreasury and Controller model. // @author Ken Silverman // Deployed by Maleeha Naveed // This deployment is for Del Norte Holdings, Delaware and US Fintech, LLC NY. // Permission to change metadata stored on blockchain explorers and elsewhere granted to: // Del Norte Holdings, DE only and/or US Fintech, LLC NY independently. Hi mom! We did it! Love you! pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Permit.sol"; import "./ControllerTree.sol"; import "./PeerTreasury.sol";
/// @title NationalPropertyManager /// @author Ken Silverman /// @notice Global property management system using ERC721 with hierarchical access control contract NationalPropertyManager is ERC721, ERC721Permit, PeerTreasury { ControllerTree public controllerTree; // --- Token metadata --- struct TokenMetadata { string country; // Branch string province; // Leaf } mapping(uint256 => TokenMetadata) public tokenMetadata;
// --- Document / Version Titles --- string[] public systemDocTitleList; mapping(string => bool) public systemDocTitles;
string[] public ownerDocTitleList; mapping(string => bool) public ownerDocTitles;
string[] public allowedVersions; mapping(string => bool) public versionAllowed;
// --- ERC-20 Mapping for Company Ownership --- struct TokenCompany { uint256 tokenId; string symbol; address erc20Address; } TokenCompany[] public tokenCompanies; mapping(uint256 => address) private tokenIdToErc20;
// --- Structs & Storage --- struct Document { string cid; // IPFS CID pointer string title; string version; uint256 timestamp; }
struct Action { string title; string version; string action; string actionNote; uint256 timestamp; }
// Mapping: tokenId => arrays of docs & history mapping(uint256 => Document[]) private _systemDocs; mapping(uint256 => Document[]) private _ownerDocs; mapping(uint256 => Action[]) private _systemHistory; mapping(uint256 => Action[]) private _ownerHistory;
// --- Events --- event SystemDocumentEvent(uint256 indexed tokenId, string title, string message); event OwnerDocumentEvent(uint256 indexed tokenId, string title, string message); event DocTitleAdded(string category, string title); event DocVersionAdded(string version); event Minted(address to, uint256 tokenId, string country, string province); event Erc20Added(uint256 indexed tokenId, string symbol, address erc20Address);
/// @notice Constructor sets up controller tree and initializes base document types /// @param _rootController Address of top-level Controller contract constructor(address _rootController) ERC721("Global Property Manager", "GPM") ERC721Permit("Global Property Manager") { // Initialize controller tree controllerTree = new ControllerTree(_rootController);
// Initialize base system doc titles _addSystemDocTitle("Tax Bill"); _addSystemDocTitle("Tax Bill Paid Receipt"); _addSystemDocTitle("Property Transferred"); _addSystemDocTitle("Property Registered");
// Initialize base owner doc titles _addOwnerDocTitle("Deed"); _addOwnerDocTitle("Survey"); _addOwnerDocTitle("House Photo"); _addOwnerDocTitle("Permit Application"); _addOwnerDocTitle("Wetlands Application"); _addOwnerDocTitle("Variance Application"); _addOwnerDocTitle("Construction Permit Application"); _addOwnerDocTitle("Subdivision Application"); _addOwnerDocTitle("Tax Bill Payment");
// Initialize base versions string[9] memory baseVersions = ["Timeless","2023","2024","2025","2026","2027","2028","2029","2030"]; for (uint i = 0; i < baseVersions.length; i++) { _addDocVersion(baseVersions[i]); } createWorldTreeStructure(_rootController); }
/// @dev Encapsulates calls to build the world → country → province hierarchy function createWorldTreeStructure(address treeAdminController) internal { // First define the level hierarchy controllerTree.addNodeLevel("Root", "Planets"); controllerTree.addNodeLevel("Planets", "Country"); controllerTree.addNodeLevel("Country", "Province"); controllerTree.addNodeLevel("Province", "District"); // 1) "World" node under "Root" at Global level levelName,parentNodeName,childNodeName,controllerAddress controllerTree.addNode("Planets", "Root", "Earth", treeAdminController); // 2) Individual countries - all at Country level with parent "World" at Global level controllerTree.addNode("Country", "Earth", "USA", treeAdminController); controllerTree.addNode("Country", "Earth", "BRA", treeAdminController); controllerTree.addNode("Country", "Earth", "SLV", treeAdminController); controllerTree.addNode("Country", "Earth", "HND", treeAdminController); controllerTree.addNode("Country", "Earth", "MEX", treeAdminController); controllerTree.addNode("Country", "Earth", "CRI", treeAdminController); // 3) Provinces - all at Province level with parents at Country level controllerTree.addNode("Province", "CRI", "Puntarenas", treeAdminController); controllerTree.addNode("Province", "CRI", "San Jose", treeAdminController); controllerTree.addNode("Province", "CRI", "Coto Brus", treeAdminController); controllerTree.addNode("Province", "CRI", "Golfito", treeAdminController); // 4) District - at District level with parent at Province level controllerTree.addNode("District", "San Jose", "Escazu", treeAdminController); }
// --- Helper functions for checking permissions --- function isRootAdmin(address addr) public view returns (bool) { address controller = controllerTree.getController("Root", "Root"); return IController(controller).isOfficialEntity("RootAdmin", addr); }
function isCountryAdmin(string memory country, address addr) public view returns (bool) { address controller = controllerTree.getController("Country", country); return IController(controller).isOfficialEntity("CountryAdmin", addr); }
function isProvinceAdmin(string memory province, address addr) public view returns (bool) { address controller = controllerTree.getController("Province", province); return IController(controller).isOfficialEntity("ProvinceAdmin", addr); }
function isDistrictEmployee(string memory district, address addr) public view returns (bool) { address controller = controllerTree.getController("District", district); return IController(controller).isOfficialEntity("DistrictEmployee", addr); }
// --- Internal helpers for initialization --- function _addSystemDocTitle(string memory title) internal { systemDocTitles[title] = true; systemDocTitleList.push(title); } function _addOwnerDocTitle(string memory title) internal { ownerDocTitles[title] = true; ownerDocTitleList.push(title); } function _addDocVersion(string memory version) internal { versionAllowed[version] = true; allowedVersions.push(version); }
// --- Admin functions: add titles/versions --- function addSystemDocTitle(string calldata title) external { // Only RootAdmin can add global document titles require(isRootAdmin(msg.sender), "NationalPropertyManager: unauthorized"); require(!systemDocTitles[title], "NationalPropertyManager: title exists"); _addSystemDocTitle(title); emit DocTitleAdded("system", title); } function addOwnerDocTitle(string calldata title) external { // Only RootAdmin can add global document titles require(isRootAdmin(msg.sender), "NationalPropertyManager: unauthorized"); require(!ownerDocTitles[title], "NationalPropertyManager: title exists"); _addOwnerDocTitle(title); emit DocTitleAdded("owner", title); } function addDocVersion(string calldata version) external { // Only RootAdmin can add global versions require(isRootAdmin(msg.sender), "NationalPropertyManager: unauthorized"); require(!versionAllowed[version], "NationalPropertyManager: version exists"); _addDocVersion(version); emit DocVersionAdded(version); }
// --- Controller Management Passthrough Functions --- function renameRoot(string calldata newName) external { // Verify caller is RootAdmin (control delegated to controllerTree) controllerTree.renameRoot(newName); } function addBranch(string calldata branchName, string calldata parentBranch) external { // Control delegated to controllerTree, which will check permissions controllerTree.addBranch(branchName, parentBranch); } function addLeaf(string calldata branchName, string calldata leafName, address controllerAddress) external { // Control delegated to controllerTree, which will check permissions controllerTree.addLeaf(branchName, leafName, controllerAddress); } // --- ERC-20 management for company ownership --- function addErc20(uint256 tokenId, string calldata symbol, address erc20Address) external returns (bool) { require(ownerOf(tokenId) == msg.sender, "NationalPropertyManager: not token owner"); require(tokenIdToErc20[tokenId] == address(0), "NationalPropertyManager: ERC-20 already registered"); require(erc20Address != address(0), "NationalPropertyManager: invalid ERC-20 address"); tokenIdToErc20[tokenId] = erc20Address; tokenCompanies.push(TokenCompany(tokenId, symbol, erc20Address)); emit Erc20Added(tokenId, symbol, erc20Address); return true; } function getErc20Address(uint256 tokenId) external view returns (address) { return tokenIdToErc20[tokenId]; } function getAllTokenCompanies() external view returns (TokenCompany[] memory) { return tokenCompanies; }
// --- Minting --- ACCEPT $2 USDC or 20 DTV -- MUST approve the amount first (likely a large mount will be approved already from district) /// @notice Mint a new NFT with country and province metadata function mint(address to, uint256 tokenId, string calldata country, string calldata province, string calldata district) external { // Check if caller is districtEmployee for the specified province require(isDistrictEmployee(country, province, msg.sender),"NationalPropertyManager: caller is not ProvinceAdmin for this province"); _safeMint(to, tokenId); // Store metadata tokenMetadata[tokenId] = TokenMetadata(country, province); emit Minted(to, tokenId, country, province); }
// --- Document management --- /// @notice Add a system document (record IPFS CID + log action) function addSystemDocument(uint256 tokenId,string calldata cid,string calldata title,string calldata version, string calldata action,string calldata actionNote,address expectedOwner) external returns (string memory) { TokenMetadata memory metadata = tokenMetadata[tokenId]; require(isDistrictEmployee(metadata.country, metadata.province, msg.sender),"NationalPropertyManager: caller is not DistrictEmployee for this province"); require(systemDocTitles[title], "NationalPropertyManager: invalid title"); require(versionAllowed[version], "NationalPropertyManager: invalid version"); address currentOwner = ownerOf(tokenId); string memory message = ""; if (currentOwner != expectedOwner) { message = "OwnerChanged"; } // Store doc and action _systemDocs[tokenId].push(Document(cid, title, version, block.timestamp)); _systemHistory[tokenId].push(Action(title, version, action, actionNote, block.timestamp)); emit SystemDocumentEvent(tokenId, title, message); return message; }
/// @notice Add an owner document; only NFT owner may call function addOwnerDocument(uint256 tokenId,string calldata cid,string calldata title,string calldata version, string calldata action, string calldata actionNote) external returns (string memory) { require(ownerOf(tokenId) == msg.sender, "NationalPropertyManager: not owner"); require(ownerDocTitles[title], "NationalPropertyManager: invalid title"); require(versionAllowed[version], "NationalPropertyManager: invalid version"); // Store doc and action _ownerDocs[tokenId].push(Document(cid, title, version, block.timestamp)); _ownerHistory[tokenId].push(Action(title, version, action, actionNote, block.timestamp)); string memory message = ""; emit OwnerDocumentEvent(tokenId, title, message); return message; }
// --- Getters --- function getSystemDocs(uint256 tokenId) external view returns (Document[] memory) { return _systemDocs[tokenId]; } function getOwnerDocs(uint256 tokenId) external view returns (Document[] memory) { return _ownerDocs[tokenId]; } function getSystemHistory(uint256 tokenId) external view returns (Action[] memory) { return _systemHistory[tokenId]; } function getOwnerHistory(uint256 tokenId) external view returns (Action[] memory) { return _ownerHistory[tokenId]; }
function getSystemDocTitles() external view returns (string[] memory) { return systemDocTitleList; } function getOwnerDocTitles() external view returns (string[] memory) { return ownerDocTitleList; } function getAllowedVersions() external view returns (string[] memory) { return allowedVersions; }
/// @notice Check if a system title is valid function isSystemDocTitle(string calldata title) external view returns (bool) { return systemDocTitles[title]; } /// @notice Check if an owner title is valid function isOwnerDocTitle(string calldata title) external view returns (bool) { return ownerDocTitles[title]; } /// @notice Check if a version is allowed function isVersionAllowed(string calldata version) external view returns (bool) { return versionAllowed[version]; } }
|
3. Subdividing Property
-
Description:
Creates new token IDs for subdivisions, updates the subdivision map, and changes the state of the original token.
4. Merging Properties
-
Description:
Merges multiple token IDs into a new token ID and updates the merge map.
function merge(
uint256[] memory tokenIds,
uint256 newTokenId,
Metadata memory newMetadata
) public onlyMunicipality {
for (uint256 i = 0; i < tokenIds.length; i++) {
require(_exists(tokenIds[i]), "One of the tokens does not exist");
stateMap[tokenIds[i]] = "Merged";
mergeMap[tokenIds[i]] = newTokenId;
}
mintProperty(newTokenId, msg.sender, newMetadata);
stateMap[newTokenId] = "Active";
}
5. Retrieving Metadata
-
Description:
Retrieves the metadata for a given token ID.
function getMetadata(uint256 tokenId) public view returns (Metadata memory) {
require(_exists(tokenId), "Token does not exist");
return metadataMap[tokenId];
}
4.3 Structures
1. tokenIdMap
- mapping(uint256 => address)
2. metadataMap
- mapping(uint256 => Metadata)
3. Metadata has been redesigned to s simpler flow. Only docs have CIDs
4. Subdivision and Merge Tracking
- subdivisionMap: mapping(uint256 => uint256[])
- mergeMap: mapping(uint256 => uint256)
5. State Tracking
- stateMap: mapping(uint256 => string) {Active, Subdivided, Merged}
4.4 The MRS_SC ERC-721 Smart Contract Implementation
for (uint256 i = 0; i < update.permitDocsArr.length; i++) {
// Add only if CID does not already exist
if (!_cidExists(metadataMap[tokenId].permitDocsArr, update.permitDocsArr[i])) {
metadataMap[tokenId].permitDocsArr.push(update.permitDocsArr[i]);
}
}
} else if (fieldHash == HASH_PROP_IMAGES_ARR) {
for (uint256 i = 0; i < update.propImagesArr.length; i++) {
if (!_cidExists(metadataMap[tokenId].propImagesArr, update.propImagesArr[i])) {
metadataMap[tokenId].propImagesArr.push(update.propImagesArr[i]);
}
}
} else if (fieldHash == HASH_PROP_TAX_RECORDS_ARR) {
for (uint256 i = 0; i < update.propTaxRecordsArr.length; i++) {
if (!_cidExists(metadataMap[tokenId].propTaxRecordsArr, update.propTaxRecordsArr[i])) {
metadataMap[tokenId].propTaxRecordsArr.push(update.propTaxRecordsArr[i]);
}
}
} else {
revert("Invalid field name");
}
}
// Utility function to check if a CID already exists in an array
function _cidExists(string[] storage cidArray, string memory cid) internal view returns (bool) {
for (uint256 i = 0; i < cidArray.length; i++) {
if (keccak256(bytes(cidArray[i])) == keccak256(bytes(cid))) {
return true; // CID already exists
}
}
return false; // CID does not exist
}
How the updateMetaData method works:
Initial State:
-
metadataMap[tokenId].permitDocsArr: ["CID1", "CID2"]
Input:
-
update.permitDocsArr: ["CID2", "CID3"]
Result:
-
After merging:
metadataMap[tokenId].permitDocsArr: ["CID1", "CID2", "CID3"]
Gas Optimization:
-
The _cidExists function adds a small amount of gas for each CID comparison, but it ensures no redundant data is stored, which is crucial for long-term cost efficiency.
6. Notes
- CID Pinning:
- All CIDs (metadata and categories) are pinned on IPFS to guarantee data availability.
- Efficient Metadata Updates:
- Allow updates to specific categories (e.g., tax records or images) by updating the corresponding CID in the PropertyData structure:
use ONLY UPDATEMETADATA method to do this
- Use IPFS URLs (Optional):
- If users or tools prefer URLs, store ipfs://CID strings directly in the mappings.
7. Added methods for precise cid lookup resembling tokenURI()
Once again, the Metadata Structure:
struct Doc {
string title; // "2025_tax_bill"
string cid; // "QmExampleCIDFor2025TaxBill"
}
// see main contract for all 9 or 10 metadata types
struct PropertyData {
Doc[] taxDocsArr; // Array of tax-related documents
Doc[] imagesArr; // Array of image metadata
Doc[] otherDocsArr; // Array of other documents
Doc[] permitDocsArr
}
mapping(uint256 => PropertyData) private propertyData; // tokenId -> PropertyData
2. Custom Lookup Method
You can write a method to retrieve a CID based on the type and title:
Implementation:
Optimized getCID Function with Precomputed Hashes
// TODO remove hashes – caller should pass in hashed value of category type
// Precomputed hashes for all 10 property data types
// since cannot use strings TODO change this to an int map so no keccak is used
// RIDICULOUS! No need for hashes here. Save gas, caller - pass the hash.
bytes32 private constant HASH_TAX_DOCS = keccak256("taxDocs"); // Hash of "taxDocs"
bytes32 private constant HASH_IMAGES = keccak256("images"); // Hash of "images"
bytes32 private constant HASH_OTHER_DOCS = keccak256("otherDocs");//Hash "otherDocs"
function getCID(uint256 tokenId, string memory docType, string memory title) public view returns (string memory) {
require(_exists(tokenId), "Token does not exist");
PropertyData storage data = propertyData[tokenId];
Doc[] storage docs;
// Hash the docType once
bytes32 docTypeHash = keccak256(abi.encodePacked(docType));
// Match the hashed docType with precomputed hashes
if (docTypeHash == HASH_TAX_DOCS) {
docs = data.taxDocs;
} else if (docTypeHash == HASH_IMAGES) {
docs = data.images;
} else if (docTypeHash == HASH_OTHER_DOCS) {
docs = data.otherDocs;
} else {
revert("Invalid document type");
}
// Search for the title
for (uint256 i = 0; i < docs.length; i++) {
if (keccak256(bytes(docs[i].title)) == keccak256(bytes(title))) {
return docs[i].cid;
}
}
revert("CID not found for the given title");
}
Example Usage
Initial State
propertyData[tokenId].taxDocs:
[
Doc({ title: "2025_tax_bill", cid: "QmExampleCID1" }),
Doc({ title: "2026_tax_bill", cid: "QmExampleCID2" })
Input
- tokenId: 1
- docType: "taxDocs"
- title: "2025_tax_bill"
Output
6. Municipality NFT System with IPFS Integration
Table of Contents
- Metadata Structure and Workflow
- Smart Contract Design
- IPFS Cluster Setup
- Sharding Logic and Dynamic Replication
- Linking Logic and Record Management
- Full Code Implementation Examples
- System Architecture and Workflows
- Considerations and Future Enhancements
1. Metadata Structure and Workflow
The metadata system uses a flat structure to simplify updates and ensures that every section of metadata is modular and independent. While flat, each section retains individual CIDs for distinct data types. This modularity allows:
- Efficient updates to specific metadata sections.
- Retention of historical data by appending new CIDs to arrays.
- Linking of records and documents for easy retrieval.
- Inclusion of Property ID and NFT Token ID in every metadata entry for redundancy and traceability.
Metadata Sections
The metadata for each property NFT is divided into the following sections:
- imageDocs: Stores one CID per image (PDF format).
- taxDocs: Stores one CID per tax document (PDF format).
- taxRecord: Logs tax assessments, payments, and statuses.
- locationRecord: Contains property address and lot number.
- permitDocs: Stores permits as individual CIDs.
- propertyCharacteristics: Records details such as size, terrain, and ownership.
- paymentRecords: Tracks payments, linking them to corresponding documents.
Each CID in these sections is stored as a paired map:
This ensures bidirectional lookup, enabling retrieval of relevant documents or records using either the CID or the title.
Metadata Update Workflow
- Addition of New Metadata:
- A new CID is generated for the updated content (e.g., a tax bill).
- The CID is added to the relevant section array without overwriting existing CIDs.
- Editing Existing Metadata:
- Only the updated section is rewritten with a new CID.
- Historical CIDs remain unchanged, ensuring immutability.
- Workflow Dependency:
- IPFS Updates Before NFT Changes: All updates to metadata must first be written to IPFS. The new CIDs are generated and validated before any changes are minted or updated on the NFT.
- Central Backup: The MPM (Municipality Property Manager) writes the updated data to Google Cloud as a central backup before minting or updating the NFT. This ensures redundancy and a fallback mechanism.
- Example Metadata Structure:
- {
- "imageDocs": [
- { "cid": "QmImage1", "title": "Front View", "propertyId": "PROP123", "tokenId": 1 },
- { "cid": "QmImage2", "title": "Rear View", "propertyId": "PROP123", "tokenId": 1 }
- ],
- "taxDocs": [
- { "cid": "QmTaxDoc2025", "title": "2025 Tax Bill", "propertyId": "PROP123", "tokenId": 1 }
- ],
- "taxRecord": [
- {
- "year": 2025,
- "assessedValue": 320000,
- "paid": false,
- "cid": "QmTaxRecord2025",
- "propertyId": "PROP123",
- "tokenId": 1
- }
- ],
- "locationRecord": {
- "address": "123 Main St",
- "lotNumber": "456",
- "cid": "QmLocationRecord",
- "propertyId": "PROP123",
- "tokenId": 1
- },
- "permitDocs": [
- { "cid": "QmPermit1", "title": "Electrical Permit", "propertyId": "PROP123", "tokenId": 1 }
- ],
- "propertyCharacteristics": {
- "size": 1500,
- "terrain": "level",
- "owner": "John Doe",
- "cid": "QmPropertyCharacteristics",
- "propertyId": "PROP123",
- "tokenId": 1
- },
- "paymentRecords": [
- { "cid": "QmPayment1", "title": "2025 Tax Payment", "amount": 320000, "propertyId": "PROP123", "tokenId": 1 }
- ]
- }
2. Smart Contract Design
The smart contract ensures secure updates to the NFT metadata and enforces role-based permissions.
Key Features
- Metadata Updates:
- Municipality can update global metadata (e.g., tax records).
- Property owners can update public metadata (e.g., images).
- Immutable History:
- Historical CIDs are retained in arrays for auditability.
- Role-Based Access:
- Municipality and property owners have distinct roles.
- Linking Logic:
- Bidirectional mapping between CIDs and titles.
- Redundant Mapping:
- propertyId:tokenId and tokenId:propertyId mappings are maintained to ensure traceability.
Smart Contract Code
- pragma solidity ^0.8.0;
- import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
- import "@openzeppelin/contracts/access/Ownable.sol";
- contract MunicipalityNFT is ERC721URIStorage, Ownable {
- struct Metadata {
- string cid;
- string title;
- string propertyId;
- uint256 tokenId;
- }
- struct Property {
- Metadata[] imageDocs;
- Metadata[] taxDocs;
- Metadata[] permitDocs;
- Metadata[] paymentRecords;
- string[] taxRecord;
- string locationRecord;
- string propertyCharacteristics;
- }
- mapping(uint256 => Property) public properties;
- mapping(uint256 => address) public propertyOwners;
- mapping(string => uint256) public propertyIdToTokenId;
- mapping(uint256 => string) public tokenIdToPropertyId;
- constructor() ERC721("MunicipalityNFT", "MUNI") {}
- function mintPropertyNFT(address owner, uint256 tokenId, string memory propertyId, string memory initialMetadata) public onlyOwner {
- _mint(owner, tokenId);
- _setTokenURI(tokenId, initialMetadata);
- propertyOwners[tokenId] = owner;
- propertyIdToTokenId[propertyId] = tokenId;
- tokenIdToPropertyId[tokenId] = propertyId;
- }
- function updateMetadata(uint256 tokenId, string memory cid, string memory section) public {
- require(_exists(tokenId), "NFT does not exist");
- if (msg.sender == owner()) {
- // Municipality updates
- if (keccak256(abi.encodePacked(section)) == keccak256("taxRecord")) {
- properties[tokenId].taxRecord.push(cid);
- } else if (keccak256(abi.encodePacked(section)) == keccak256("locationRecord")) {
- properties[tokenId].locationRecord = cid;
- } else {
- revert("Invalid section for municipality");
- }
- } else if (msg.sender == propertyOwners[tokenId]) {
- // Property owner updates
- revert("Owner updates not supported yet");
- } else {
- revert("Unauthorized");
- }
- _setTokenURI(tokenId, cid);
- }
- function getMetadata(uint256 tokenId) public view returns (Property memory) {
- return properties[tokenId];
- }
- }
3. IPFS Cluster Setup
Cluster Configuration
- Node Setup:
- Each municipality employee runs a full IPFS node.
- Nodes form a private swarm using a shared swarm key.
- Swarm Key Management:
- The swarm key is securely stored and managed by the municipality.
- Data Replication:
- Nodes replicate data for redundancy.
- Replication level (isomorphism) adjusts dynamically based on storage:
- Level 1 (Full Replication): All nodes store all data.
- Level 0.5 (Halved Replication): Each node stores 50% of data.
- Level 0.25 (Quarter Replication): Each node stores 25% of data.
- When storage exceeds 500GB on a node, all nodes must agree to reduce the replication level.
- Backup Nodes:
- Two dedicated backup nodes always maintain full replication.
- Google Cloud Backup:
- All data is redundantly backed up to Google Cloud for disaster recovery.
IPFS Node Configuration
- {
- "Datastore": {
- "StorageMax": "500GB",
- "StorageGCWatermark": 90
- }
- }
Adding Data to IPFS
- const ipfsClient = require('ipfs-http-client');
- const ipfs = ipfsClient.create({ url: 'http://localhost:5001' });
- async function addFileToIPFS(filePath) {
- const file = fs.readFileSync(filePath);
- const { cid } = await ipfs.add(file);
- console.log(`File CID: ${cid.toString()}`);
- return cid.toString();
- }
4. Sharding Logic and Dynamic Replication
Overview of Sharding Strategy
To optimize storage usage and ensure data redundancy, the system uses dynamic replication with hysteresis thresholds.
- Storage Capacity:
- Each municipality machine allocates 1TB of storage:
- 500GB reserved for IPFS data.
- 500GB for the MPM and other tasks.
- IPFS is maintained in a dedicated partition for streamlined management.
- Replication Levels (Isomorphism):
- Level 1 (Full Replication):
- All nodes store 100% of the data.
- Effective until storage exceeds 500GB.
- Level 0.5 (Halved Replication):
- Each node stores 50% of the data.
- At least 50% of nodes must be online to ensure full data availability.
- Level 0.25 (Quarter Replication):
- Each node stores 25% of the data.
- At least 25% of nodes must be online to ensure full data availability.
- Hysteresis Thresholds:
- Nodes reduce replication levels only when storage exceeds 500GB.
- Nodes return to higher replication levels when storage drops below 256GB.
- Backup Nodes:
- Two dedicated backup nodes maintain full replication at all times.
- Each backup node has 10TB of storage capacity.
- Example Scenario:
- 100 nodes, each storing 500GB (full replication).
- When the first node exceeds 500GB, all nodes agree to reduce replication to 50%.
- Each node now stores 256GB, requiring at least 50 nodes to ensure full data availability.
- Transition to Level 0.25:
- When nodes exceed 500GB again, replication reduces to 25%.
- Each node stores 125GB, requiring at least 25 nodes for full availability.
Benefits of Sharding
- Optimized Storage:
- Prevents data duplication and maximizes available storage.
- Dynamic Scaling:
- Adjusts to the number of nodes and storage availability.
- Redundancy:
- Ensures data is always available, even during node failures.
- Disaster Recovery:
- Backup nodes and Google Cloud provide additional safety nets.
5. Linking Logic and Record Management
Bidirectional Mapping
Every CID is linked to a title using paired arrays or maps:
- cid:title
- title:cid
- propertyId:tokenId
- tokenId:propertyId
Example CID Mapping
- {
- "cid:title": {
- "QmTaxDoc2025": "2025 Tax Bill",
- "QmImage1": "Front View"
- },
- "title:cid": {
- "2025 Tax Bill": "QmTaxDoc2025",
- "Front View": "QmImage1"
- },
- "propertyId:tokenId": {
- "PROP123": 1
- },
- "tokenId:propertyId": {
- "1": "PROP123"
- }
- }
6. System Architecture and Workflows
- Minting Property NFTs:
- Municipality mints a one-time NFT per property.
- Updating Metadata:
- Metadata is updated in IPFS, backed up to Google Cloud, and the smart contract stores the new CID.
- Viewing Metadata:
- Users can view metadata via a blockchain explorer or retrieve data directly from IPFS.
- Ensuring Data Integrity:
- Historical CIDs are retained and auditable.
7. Considerations and Future Enhancements
- Scaling:
- Implement sharding for large datasets.
- Security:
- Use secure key management for IPFS swarm and smart contract access.
- Role-Based Updates:
- Extend functionality for property owner updates.
- Real-Time Notifications:
- Notify users of metadata updates or payments via integrated apps.
TESTS
salesContract:
Test1) Tony plays registrar. approvalMax=”3000” Kens sends Tony a message myPublicAddress+” “+3000 as an email or as a google doc questionnaire
Tony responds with signedMessage = sign(message = keccack(tony’s PubAddr+” “+ken’s pubAddr+” “+”3000”))
No nonce required because this only happens once per user.
Ken submits message to sales contract
purchase(message,signedMessage) {
Check that keccack(message) = signedMessage
Check that msg.sender = message’s section 2
Check that msg.sender = ecRecover(signedMessage,
Javascript Library, blockchain explorer methods for management
APPROVAL LIMITS for smart contracts to act on treasurey addresses.
SEE all approval limits on all of 5 core treas addresses and contracts by name.
Login and Device Key Authorization Flow
The login strategy leverages Ethereum-compatible elliptic curve (EC) cryptography for secure, efficient user authentication. Users can choose between wallet-based interactions (via MetaMask or similar wallets) or device-generated EC key pairs with encrypted private keys stored client-side.
Key Management and Security Features
- Public Keys as Identifiers: Public keys serve dual purposes—user authentication and employee-based transaction authorization, allowing the spending of small amounts for authorized transactions. These keys directly link to employee identities stored in NFT-based smart contracts (ERC-721).
- Account Recovery and Key Management:
- Employees can regenerate keys on-demand using an automated client-side key generation tool pre-login, validated by an email verification code workflow.
- Alternatively, automated key regeneration via email verification can be disabled by any employee with admin level 6+. In this mode, key additions or removals are solely permitted through an administrator (level 6+).
- Administrators can also flag users as restricted or remove user access entirely, subject to compliance standards. No records are permanently deleted; instead, records are flagged inactive to maintain robust audit trails.
Enhanced Login Workflow
The authentication workflow ensures robust defense against replay attacks and unauthorized access:
- Client-side Generation and Submission
- User signs a message: signedMessage = sign(unixTime + " " + pubKey)
- Submission payload:
{ "unixTime": "1710423214000", "pubKey": "<hex-formatted-pubkey>", "signedMessage": "<signature-hex>" } |
Server-side Verification Flow
public static function validateSignedMessage($signedMessageJSONObj, $expectedPubKey, $refDateTime, $maxMinutes = 30) { $signedData = json_decode($signedMessageJSONObj, true); if (!$signedData || !isset($signedData['unixTime'], $signedData['pubKey'], $signedData['signedMessage'])) { return "Invalid signed message format."; }
if ($signedData['pubKey'] !== $expectedPubKey) { return "Public key mismatch."; }
$signedMillis = (float)$signedData['unixTime']; $msgTime = (int)($signedMillis / 1000); $lastLoginTimestamp = (new DateTime($refDateTime, new DateTimeZone(date_default_timezone_get()))) ->setTimezone(new DateTimeZone("UTC"))->getTimestamp();
if ($msgTime < $lastLoginTimestamp || abs(time() - $msgTime) > $maxMinutes * 60) { return "Timestamp validation failed."; }
$rawMessage = $signedData['unixTime'] . " " . $signedData['pubKey']; return WalletUtils::verifySignedMessage($expectedPubKey, $signedData['signedMessage'], $rawMessage); } |
public static function verifySignedMessage($pubKey, $signature, $message) { $computedHash = Keccak::hash($message, 256); $recoveredPubKey = self::ecrecover($computedHash, $signature); if ($recoveredPubKey && strtolower($recoveredPubKey) === strtolower($pubKey)) { return false; // Successful verification } return "Signature verification failed."; } |
Replay Attack Mitigation
- Even possessing the email, public key, and a previously signed message, replay attacks are thwarted by timestamp validation:
- The server verifies that the provided unixTime exceeds the previous login timestamp.
- A message replay with stale timestamps will fail authentication.
Nonce-based Challenge (High-Security Option)
- In defense-grade environments, a nonce-based challenge can be implemented:
- Every AJAX request can require client-side signing of a unique server-provided nonce.
- This increases security but slightly reduces performance for frequent requests.
Client Server | | | Generate unixTime, pubKey| | Sign(unixTime + pubKey) | |------------------------->| | | | Validate timestamp | | Validate pubKey | | Verify signedMessage | | (via ecrecover or verify)| |<-------------------------| | | | Session ID (6 min expiry)| |
Administrative Key Control
- Automated Recovery: (Default) User-driven key recovery with email verification.
- Admin-Controlled Recovery: (Optional) Level 6+ administrators can manually manage keys, bypassing email verification.
- Admin can disable user keys, restrict users, or remove user access completely.
- New user keys can only be added by admin if user keys are lost, ensuring strict administrative oversight.
Data Integrity and Audit Compliance
- All changes and user activity logs are immutable.
- Deletion of records is disabled; instead, records can be flagged as inactive or restricted, maintaining rigorous audit compliance.
This dual-purpose system provides both streamlined user convenience and stringent security controls suitable for government-grade environments.
ERC-721 Smart Contracts Management
- REG-TEAM-SC: Dedicated smart contract for employee (TeamMembers) management.
- Stores Employee Name, ID, Company ID, and Domain.
- Employee public keys link directly to minted NFTs representing authorization status.
- END-USERS-SC: Manages end-users (no-login roles, e.g., property owners).
- Similar metadata structure to REG-TEAM-SC.
- Public keys represent registered user status and access roles.
- Both contracts include deployment metadata:
- Deploying entity’s name, company ID, and official domain.
- Deployer’s personal name and public address for verification.
- Smart contract addresses linked to entity profiles on official websites, verifying authenticity.
AI Audit (Ongoing) GPT 4.5 review of all deployed contracts.
Smart Contract Audit Report for Del Norte Network
Overview
The smart contract suite provided implements a sophisticated, role-based treasury and token management system for Del Norte Network. The architecture includes multiple smart contracts:
- DelNorteToken: An ERC20 token contract (DTV/PDTV) with whitelisting and gating mechanisms.
- ElasticTreasuryHub: Central treasury management contract handling administrative transfers and peer-to-peer treasury transactions.
- PeerTreasury: Facilitates peer treasury transactions (ETH and ERC20 tokens) between official smart contracts.
- ReleaseManager: Manages scheduled token and ETH payments (vesting schedules) for employees and contractors.
- Controller: Manages official entities and roles for authorization and access control.
- Recoverable: Allows recovery of accidental transfers with a fee.
Key Functionality and Workflows
1. Role-Based Access Control (RBAC)
RBAC is implemented via the Controller contract. Official roles include:
- TreasuryAdmin: Controls financial flows and executive withdrawals.
- TokenAdmin: Manages token-related administrative tasks.
- Registrar: Handles user whitelisting and registration.
- SmartContract: Ensures transactions between official contracts only.
This RBAC model effectively prevents unauthorized interactions.
2. DelNorteToken (DTV/PDTV)
- Implements ERC20 standard with additional gating to restrict transfers.
- Users must be whitelisted by a Registrar. Whitelisting includes off-chain KYC verification signed by the Registrar, enforced through whitelistUserWithRegKey().
- The gating mechanism can be toggled (gatingActive) to allow unrestricted transfers temporarily (used during launchpad events).
3. ElasticTreasuryHub
- Centralizes token and ETH treasury management, ensuring only TreasuryAdmin can initiate transfers.
- Implements executiveWithdrawETH and executiveWithdrawTokens methods for direct administrative transfers outside regular treasury flows, suitable for emergency or capital expense transfers to external wallets or launchpads.
4. PeerTreasury
- Manages secure ETH and ERC20 token transfers between official smart contracts, ensuring robust and traceable internal treasury management.
- Requires proper official entity status (TreasuryAdmin or SmartContract) to execute transfers.
5. ReleaseManager
- Manages vesting schedules for both tokens and ETH.
- Receives tokens and USDC (via PeerTreasury) for scheduled distributions to employees and contractors.
- Ensures token vesting schedules (addReleaseSchedule) can only be created and modified by authorized entities.
- Allows employees/contractors to claim vested amounts according to predefined schedules.
- Implements protections against over-commitment of treasury funds (globalAmountOwed).
Executive Withdrawals Explained
The executiveWithdrawETH and executiveWithdrawTokens methods in ElasticTreasuryHub allow TreasuryAdmin to directly move ETH or tokens out of the Hub. These functions:
- Verify available balances before transfers.
- Use secure transfer mechanisms (call for ETH and standard ERC20 transfer method).
- Emit events for auditability and transparency.
This mechanism enables direct token distribution to launchpads or other external entities, independent of internal peer-to-peer transfers or scheduled releases.
Intended Workflows
Token Sales and Vesting Workflow
- Users purchase tokens via a Sales contract (to be developed).
- Sales contract verifies buyer registration via the Token contract (isUnrestrictedWhitelistedUser).
- Upon successful purchase, the Sales contract initiates a treasury transfer (USDC) via PeerTreasury to the ReleaseManager.
- ReleaseManager creates scheduled payment distributions to registered and whitelisted users.
User Whitelisting Workflow
- Registrars (approved via Controller) whitelist users using off-chain signatures.
- Users register on-chain by presenting Registrar-signed messages via whitelistUserWithRegKey.
- Whitelisted status allows users to receive and transfer tokens freely within the gating rules.
Launchpad and PDTV Token Workflow
- PDTV token launch is initiated with gating temporarily disabled (gatingActive=false).
- Post-launch, gating is re-enabled.
- PDTV holders with ≥6000 tokens register via Registrar to enable token swaps to DTV tokens.
- Registration ensures only approved, whitelisted users can swap PDTV to DTV, reducing administrative overhead.
Security Considerations and Recommendations
Strengths
- Robust RBAC implementation effectively restricts actions to authorized roles.
- Clear separation of concerns enhances modularity and maintainability.
- Comprehensive auditability through event logging.
- Accidental transfer recovery mechanisms improve user safety.
Potential Issues & Recommendations
- Reentrancy Protection: Consider adding OpenZeppelin’s ReentrancyGuard to sensitive functions (executiveWithdrawETH, executiveWithdrawTokens, claim, etc.).
- ETH Handling: Use low-level calls for ETH transfers consistently to mitigate gas limitations (transfer vs. call).
- Input Validation: Ensure complete validation in batch operations (like addReleaseScheduleList) to prevent inconsistent schedule states.
- Gas Optimization: Evaluate mappings and arrays (e.g., beneficiary lists) for optimization to avoid excessive gas costs in large data sets.
- Emergency Functions: Provide emergency pause functionality for critical administrative operations to manage unforeseen scenarios.
Conclusion
The provided smart contract system demonstrates a highly organized, secure, and comprehensive treasury and token management solution. Executive withdrawal mechanisms are correctly implemented, providing the necessary flexibility for direct external transfers, such as those to launchpads. With minor enhancements and attention to potential issues noted above, the system will robustly meet its intended workflows and security goals.
Final Status: Audit Passed with Recommendations for Improvements.