From 5e5aa4af15465baf0a0bacbfbb022d2b09e0ee69 Mon Sep 17 00:00:00 2001 From: mattjhawken Date: Thu, 18 Sep 2025 11:17:29 -0400 Subject: [PATCH 1/5] Adding DAO percentage to reward generation and fees in Token contract. --- src/SmartnodesCoordinator.sol | 3 -- src/SmartnodesToken.sol | 66 ++++++++++------------------------- test/CoordinatorTest.t.sol | 15 -------- 3 files changed, 18 insertions(+), 66 deletions(-) diff --git a/src/SmartnodesCoordinator.sol b/src/SmartnodesCoordinator.sol index 8cc12c2..535a845 100644 --- a/src/SmartnodesCoordinator.sol +++ b/src/SmartnodesCoordinator.sol @@ -269,7 +269,6 @@ contract SmartnodesCoordinator is ReentrancyGuard { // Verify proposal data integrity bytes32 computedHash = _computeProposalHash( - proposalId, merkleRoot, validatorsToRemove, jobHashes, @@ -557,7 +556,6 @@ contract SmartnodesCoordinator is ReentrancyGuard { } function _computeProposalHash( - uint8 proposalId, bytes32 merkleRoot, address[] calldata validatorsToRemove, bytes32[] calldata jobHashes, @@ -567,7 +565,6 @@ contract SmartnodesCoordinator is ReentrancyGuard { return keccak256( abi.encode( - proposalId, merkleRoot, validatorsToRemove, jobHashes, diff --git a/src/SmartnodesToken.sol b/src/SmartnodesToken.sol index c39501c..f91268e 100644 --- a/src/SmartnodesToken.sol +++ b/src/SmartnodesToken.sol @@ -38,7 +38,6 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { error Token__ETHTransferFailed(); error Token__OnlyDAO(); error Token__DAOAlreadySet(); - error Token__InvalidBiasValidator(); error Token__DistributionTooEarly(); error Token__InvalidInterval(); @@ -277,50 +276,30 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { // ============ Emissions & Halving ============ /** - * @notice Distribute validator rewards with configurable bias validator + * @notice Distribute validator rewards * @param _approvedValidators List of validators that voted * @param _validatorReward Total reward amounts for validators * @param _distributionId Current distribution ID for events - * @param _biasValidator Address of validator to receive bias (replaces tx.origin) - * @dev 10% of validator rewards go to bias validator, remaining 90% split equally among all validators - * @dev Bias validator receives both the bias amount and their equal share + * @param _dustValidator Validator that gets the scraps (proposal creator) */ function _distributeValidatorRewards( address[] memory _approvedValidators, PaymentAmounts memory _validatorReward, uint256 _distributionId, - address _biasValidator + address _dustValidator ) internal { uint8 _nValidators = uint8(_approvedValidators.length); - // Validate bias validator is in the approved list - bool biasValidatorFound = false; - for (uint256 i = 0; i < _nValidators; i++) { - if (_approvedValidators[i] == _biasValidator) { - biasValidatorFound = true; - break; - } - } - if (!biasValidatorFound) revert Token__InvalidBiasValidator(); - - // Calculate bias amount (10% of total validator reward goes to bias validator) - uint256 snoBiasAmount = (uint256(_validatorReward.sno) * 10) / 100; - uint256 ethBiasAmount = (uint256(_validatorReward.eth) * 10) / 100; - - // Remaining 90% to be split equally among all validators - uint256 snoRemainingPool = uint256(_validatorReward.sno) - - snoBiasAmount; - uint256 ethRemainingPool = uint256(_validatorReward.eth) - - ethBiasAmount; + // Remaining pool to be split equally among validators + uint256 snoPool = uint256(_validatorReward.sno); + uint256 ethPool = uint256(_validatorReward.eth); - uint256 snoPerValidator = snoRemainingPool / _nValidators; - uint256 ethPerValidator = ethRemainingPool / _nValidators; + uint256 snoPerValidator = snoPool / _nValidators; + uint256 ethPerValidator = ethPool / _nValidators; // Handle dust/remainder - uint256 snoRemainder = snoRemainingPool - - (snoPerValidator * _nValidators); - uint256 ethRemainder = ethRemainingPool - - (ethPerValidator * _nValidators); + uint256 snoRemainder = snoPool - (snoPerValidator * _nValidators); + uint256 ethRemainder = ethPool - (ethPerValidator * _nValidators); // Distribute to validators for (uint256 i = 0; i < _nValidators; i++) { @@ -330,14 +309,8 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { uint256 snoShare = snoPerValidator; uint256 ethShare = ethPerValidator; - // Bias validator gets the bias amount - if (validator == _biasValidator) { - snoShare += snoBiasAmount; - ethShare += ethBiasAmount; - } - - // First validator gets remainder to avoid dust - if (i == 0) { + // Give dust to first validator to avoid lost remainder + if (_dustValidator == validator) { snoShare += snoRemainder; ethShare += ethRemainder; } @@ -388,7 +361,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { * @param _totalCapacity Total capacity of contributed workers * @param _payments Additional payments to be added to the current emission rate * @param _approvedValidators List of validators that voted - * @param _biasValidator Address of validator to receive bias rewards + * @param _dustValidator Address of validator to receive dust rewards (usually the proposal executor) * @dev Workers receive 90% of the total reward, validators receive 10% * @dev Rewards are distributed with bias towards specified validator, then proportionally to workers based on their capacities * @dev This function is called by SmartnodesCore during state updates to distribute rewards. @@ -398,11 +371,10 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { uint256 _totalCapacity, address[] memory _approvedValidators, PaymentAmounts calldata _payments, - address _biasValidator + address _dustValidator ) external onlySmartnodesCore nonReentrant { uint8 _nValidators = uint8(_approvedValidators.length); if (_nValidators == 0) revert Token__InvalidValidatorLength(); - if (_biasValidator == address(0)) revert Token__InvalidBiasValidator(); // Total rewards to be distributed PaymentAmounts memory totalReward = PaymentAmounts({ @@ -423,7 +395,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { _approvedValidators, totalReward, distributionId, - _biasValidator + _dustValidator ); return; // exit early } @@ -469,7 +441,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { _approvedValidators, validatorReward, distributionId, - _biasValidator + _dustValidator ); } @@ -812,7 +784,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { * @param _user The address of the user whose escrowed ETH payment is being released * @param _amount The amount of escrowed ETH payment to be released * @dev This releases the escrowed ETH to be available for distribution as rewards - * @dev The ETH stays in the contract but is no longer considered "escrowed" + * @dev The ETH stays in the contract but is no longer considered escrowed */ function releaseEscrowedEthPayment( address _user, @@ -896,7 +868,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { return address(s_smartnodesCore); } - // ============ ERC20Votes Overrides ============ + // ============ Required Overrides ============ /** * @dev Override to prevent voting with locked tokens @@ -922,8 +894,6 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { return balance; } - // ============ Required Overrides ============ - /** * @dev Override required by Solidity for multiple inheritance */ diff --git a/test/CoordinatorTest.t.sol b/test/CoordinatorTest.t.sol index bba09b1..dab8e20 100644 --- a/test/CoordinatorTest.t.sol +++ b/test/CoordinatorTest.t.sol @@ -51,7 +51,6 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { bytes32 proposalHash = keccak256( abi.encode( - 1, merkleRoot, validatorsToRemove, jobHashes, @@ -148,7 +147,6 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { bytes32 proposalHash = keccak256( abi.encode( - 1, merkleRoot, validatorsToRemove, jobHashes, @@ -235,7 +233,6 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { bytes32 proposalHash = keccak256( abi.encode( - 1, merkleRoot, validatorsToRemove, jobHashes, @@ -282,7 +279,6 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { bytes32 proposalHash = keccak256( abi.encode( - 1, merkleRoot, validatorsToRemove, jobHashes, @@ -305,15 +301,4 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { console.log("Voting successful. Total votes:", proposal.votes); } - - function testCannotVoteTwice() public { - (uint128 updateTime, ) = coordinator.timeConfig(); - vm.warp(block.timestamp + updateTime * 2); - - uint256 numWorkers = 1; - ( - Participant[] memory participants, - uint256 totalCapacity - ) = _setupTestParticipants(numWorkers, false); - } } From 4ad800780bb0f7494be92686d7b740e45a1667e5 Mon Sep 17 00:00:00 2001 From: mattjhawken Date: Thu, 18 Sep 2025 12:23:01 -0400 Subject: [PATCH 2/5] Gave DAO 3% direct reward distribution --- src/SmartnodesToken.sol | 121 ++++++++++++++++++++++++---------------- test/BaseTest.sol | 1 + test/TokenTest.t.sol | 42 +++++++++----- 3 files changed, 100 insertions(+), 64 deletions(-) diff --git a/src/SmartnodesToken.sol b/src/SmartnodesToken.sol index f91268e..cb4dc5a 100644 --- a/src/SmartnodesToken.sol +++ b/src/SmartnodesToken.sol @@ -71,6 +71,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { /** Constants */ uint8 private constant VALIDATOR_REWARD_PERCENTAGE = 10; + uint8 private constant DAO_REWARD_PERCENTAGE = 3; uint256 private constant BASE_EMISSION_RATE = 5832e18; // Base hourly emission rate uint256 private constant TAIL_EMISSION = 420e18; // Base hourly tail emission uint256 private constant REWARD_PERIOD = 365 days; @@ -91,18 +92,17 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { uint256 public s_validatorLockAmount; uint256 public s_userLockAmount; - uint256 public s_currentDistributionId; + // Payment and rewards tracking (SNO + ETH) PaymentAmounts public s_totalUnclaimed; PaymentAmounts public s_totalEscrowed; - uint256 public s_totalLocked; + uint256 public s_totalLocked; // SNO + uint256 public s_totalETHDeposited; + uint256 public s_totalETHWithdrawn; + uint256 public s_currentDistributionId; uint256 public s_distributionInterval; uint256 public s_lastDistributionTime; - // ETH balance tracking - uint256 public s_totalETHDeposited; - uint256 public s_totalETHWithdrawn; - mapping(uint256 => MerkleDistribution) public s_distributions; mapping(uint256 => mapping(address => bool)) public s_claimed; mapping(address => LockedTokens) private s_lockedTokens; @@ -120,8 +120,9 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { } modifier onlyDAO() { - if (s_dao != address(0)) { - if (msg.sender != s_dao) { + address dao = s_dao; + if (dao != address(0)) { + if (msg.sender != dao) { revert Token__OnlyDAO(); } } @@ -162,6 +163,11 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { uint256 totalSno, uint256 totalEth ); + event DAORewardsDistributed( + uint256 indexed distributionId, + uint256 snoAmount, + uint256 ethAmount + ); event ETHDeposited(address indexed from, uint256 amount); event ETHWithdrawn(address indexed to, uint256 amount); event DistributionIntervalUpdated(uint256 oldInterval, uint256 newInterval); @@ -315,7 +321,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { ethShare += ethRemainder; } - _payValidator(validator, snoShare, ethShare); + _payAccount(validator, snoShare, ethShare); } emit ValidatorRewardsDistributed( @@ -332,7 +338,7 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { * @param snoAmount Amount of SNO tokens to mint/send * @param ethAmount Amount of ETH to transfer */ - function _payValidator( + function _payAccount( address validator, uint256 snoAmount, uint256 ethAmount @@ -362,9 +368,9 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { * @param _payments Additional payments to be added to the current emission rate * @param _approvedValidators List of validators that voted * @param _dustValidator Address of validator to receive dust rewards (usually the proposal executor) - * @dev Workers receive 90% of the total reward, validators receive 10% - * @dev Rewards are distributed with bias towards specified validator, then proportionally to workers based on their capacities - * @dev This function is called by SmartnodesCore during state updates to distribute rewards. + * @dev Workers receive 85% of the total reward, validators receive 10%, dao receives 5% + * @dev Rewards are distributed proportionally to workers based on their capacities. + * @dev This function is called periodically by SmartnodesCore during state updates to distribute rewards. */ function createMerkleDistribution( bytes32 _merkleRoot, @@ -389,54 +395,71 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { uint256 distributionId = ++s_currentDistributionId; - if (_totalCapacity == 0) { - // All rewards go to validators - _distributeValidatorRewards( - _approvedValidators, - totalReward, - distributionId, - _dustValidator - ); - return; // exit early - } - - // Split validator/worker share from the remaining pool - PaymentAmounts memory validatorReward = PaymentAmounts({ + // Calculate reward distributions + PaymentAmounts memory daoReward = PaymentAmounts({ sno: uint128( - (uint256(totalReward.sno) * VALIDATOR_REWARD_PERCENTAGE) / 100 + (uint256(totalReward.sno) * DAO_REWARD_PERCENTAGE) / 100 ), eth: uint128( - (uint256(totalReward.eth) * VALIDATOR_REWARD_PERCENTAGE) / 100 + (uint256(totalReward.eth) * DAO_REWARD_PERCENTAGE) / 100 ) }); + PaymentAmounts memory validatorReward; - PaymentAmounts memory workerReward = PaymentAmounts({ - sno: totalReward.sno - validatorReward.sno, - eth: totalReward.eth - validatorReward.eth - }); + if (_totalCapacity == 0) { + // If no workers, just give to validators + validatorReward = PaymentAmounts({ + sno: totalReward.sno - daoReward.sno, + eth: totalReward.eth - daoReward.eth + }); + } else { + // Split validator/worker share from the remaining pool + validatorReward = PaymentAmounts({ + sno: uint128( + (uint256(totalReward.sno) * VALIDATOR_REWARD_PERCENTAGE) / + 100 + ), + eth: uint128( + (uint256(totalReward.eth) * VALIDATOR_REWARD_PERCENTAGE) / + 100 + ) + }); - // Store merkle distribution (only worker rewards are stored for claiming) - s_distributions[distributionId] = MerkleDistribution({ - merkleRoot: _merkleRoot, - workerReward: workerReward, - totalCapacity: _totalCapacity, - active: true, - timestamp: block.timestamp - }); + PaymentAmounts memory workerReward = PaymentAmounts({ + sno: totalReward.sno - validatorReward.sno - daoReward.sno, + eth: totalReward.eth - validatorReward.eth - daoReward.eth + }); + + // Store merkle distribution (only worker rewards are stored for claiming) + s_distributions[distributionId] = MerkleDistribution({ + merkleRoot: _merkleRoot, + workerReward: workerReward, + totalCapacity: _totalCapacity, + active: true, + timestamp: block.timestamp + }); - // Update total unclaimed (only worker rewards) - s_totalUnclaimed.sno += workerReward.sno; - s_totalUnclaimed.eth += workerReward.eth; + // Update total unclaimed (only worker rewards) + s_totalUnclaimed.sno += workerReward.sno; + s_totalUnclaimed.eth += workerReward.eth; - emit MerkleDistributionCreated( + emit MerkleDistributionCreated( + distributionId, + _merkleRoot, + totalReward.sno, + totalReward.eth, + block.timestamp + ); + } + // Distribute DAO rewards + _payAccount(s_dao, daoReward.sno, daoReward.eth); + emit DAORewardsDistributed( distributionId, - _merkleRoot, - totalReward.sno, - totalReward.eth, - block.timestamp + daoReward.sno, + daoReward.eth ); - // Distribute validator rewards immediately with bias + // Distribute validator rewards _distributeValidatorRewards( _approvedValidators, validatorReward, diff --git a/test/BaseTest.sol b/test/BaseTest.sol index 5e2b72d..2e37dab 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -15,6 +15,7 @@ abstract contract BaseSmartnodesTest is Test { uint256 constant DEPLOYMENT_MULTIPLIER = 1; uint128 constant INTERVAL_SECONDS = 1 minutes; uint256 constant VALIDATOR_REWARD_PERCENTAGE = 10; + uint256 constant DAO_REWARD_PERCENTAGE = 3; uint256 constant ADDITIONAL_SNO_PAYMENT = 1000e18; uint256 constant ADDITIONAL_ETH_PAYMENT = 5 ether; uint256 constant INITIAL_EMISSION_RATE = 5832e18; diff --git a/test/TokenTest.t.sol b/test/TokenTest.t.sol index 24ee746..b58c4c5 100644 --- a/test/TokenTest.t.sol +++ b/test/TokenTest.t.sol @@ -430,8 +430,14 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { VALIDATOR_REWARD_PERCENTAGE) / 100; uint256 expectedValidatorEth = (totalEthReward * VALIDATOR_REWARD_PERCENTAGE) / 100; - uint256 expectedWorkerSno = totalSnoReward - expectedValidatorSno; - uint256 expectedWorkerEth = totalEthReward - expectedValidatorEth; + uint256 expectedDaoSno = (totalSnoReward * DAO_REWARD_PERCENTAGE) / 100; + uint256 expectedDaoEth = (totalEthReward * DAO_REWARD_PERCENTAGE) / 100; + uint256 expectedWorkerSno = totalSnoReward - + expectedValidatorSno - + expectedDaoSno; + uint256 expectedWorkerEth = totalEthReward - + expectedValidatorEth - + expectedDaoEth; assertEq( workerReward.sno, @@ -469,15 +475,18 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { ADDITIONAL_SNO_PAYMENT; uint256 totalEthReward = ADDITIONAL_ETH_PAYMENT; - // Validator rewards (already paid out directly in contract) - uint256 validatorSnoReward = (totalSnoReward * + uint256 expectedValidatorSno = (totalSnoReward * VALIDATOR_REWARD_PERCENTAGE) / 100; - uint256 validatorEthReward = (totalEthReward * + uint256 expectedValidatorEth = (totalEthReward * VALIDATOR_REWARD_PERCENTAGE) / 100; - - // Worker reward pools - uint256 expectedWorkerSno = totalSnoReward - validatorSnoReward; - uint256 expectedWorkerEth = totalEthReward - validatorEthReward; + uint256 expectedDaoSno = (totalSnoReward * DAO_REWARD_PERCENTAGE) / 100; + uint256 expectedDaoEth = (totalEthReward * DAO_REWARD_PERCENTAGE) / 100; + uint256 expectedWorkerSno = totalSnoReward - + expectedValidatorSno - + expectedDaoSno; + uint256 expectedWorkerEth = totalEthReward - + expectedValidatorEth - + expectedDaoEth; uint256 totalCapacity = 0; for (uint256 i = 0; i < participants.length; i++) { @@ -559,22 +568,25 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { // Pre-claim balances uint256 preClaimBalance = token.balanceOf(worker.addr); - uint256 preClaimEth = worker.addr.balance; // Claim rewards vm.prank(worker.addr); token.claimMerkleRewards(distributionId, worker.capacity, proof); - // Calculate expected rewards - (, SmartnodesToken.PaymentAmounts memory workerReward, , , ) = token - .s_distributions(distributionId); - uint256 validatorSnoReward = ((INITIAL_EMISSION_RATE * DEPLOYMENT_MULTIPLIER + ADDITIONAL_SNO_PAYMENT) * VALIDATOR_REWARD_PERCENTAGE) / 100; + + uint256 daoSnoReward = ((INITIAL_EMISSION_RATE * + DEPLOYMENT_MULTIPLIER + + ADDITIONAL_SNO_PAYMENT) * DAO_REWARD_PERCENTAGE) / 100; + uint256 expectedWorkerSno = (INITIAL_EMISSION_RATE * DEPLOYMENT_MULTIPLIER + - ADDITIONAL_SNO_PAYMENT) - validatorSnoReward; + ADDITIONAL_SNO_PAYMENT) - + validatorSnoReward - + daoSnoReward; + uint256 expectedWorkerSnoShare = (expectedWorkerSno * worker.capacity) / totalCapacity; From 1c272f367116c45753420ce53bc976638a867718 Mon Sep 17 00:00:00 2001 From: mattjhawken Date: Thu, 18 Sep 2025 15:14:48 -0400 Subject: [PATCH 3/5] Refactor SmartnodesToken to SmartnodesERC20 --- .DS_Store | Bin 6148 -> 6148 bytes script/Deploy.s.sol | 4 +- src/SmartnodesCoordinator.sol | 6 +- src/SmartnodesCore.sol | 6 +- src/SmartnodesDAO.sol | 19 +- ...martnodesToken.sol => SmartnodesERC20.sol} | 2 +- ...artnodesToken.sol => ISmartnodesERC20.sol} | 4 +- test/BaseTest.sol | 9 +- test/CoordinatorTest.t.sol | 6 +- test/DAOTest.sol | 180 ++++++++++++++++++ test/TokenTest.t.sol | 38 ++-- 11 files changed, 227 insertions(+), 47 deletions(-) rename src/{SmartnodesToken.sol => SmartnodesERC20.sol} (99%) rename src/interfaces/{ISmartnodesToken.sol => ISmartnodesERC20.sol} (95%) diff --git a/.DS_Store b/.DS_Store index 3f59ec82a8a48f2c415152a262b0dab6473f51b7..cdc8eda77d0d917ed493938a3994ae7aca486ea1 100644 GIT binary patch delta 26 icmZoMXfc>z$H+L*UY?P0W8yOQ$$BDeo4GlD@&f>7?FW(o delta 24 gcmZoMXfc>z$H*|zUVdZZGWN;3BJ7*FIezj30AOtgj{pDw diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 882de27..811b8a5 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.22; import {Script, console} from "forge-std/Script.sol"; import {SmartnodesCore} from "../src/SmartnodesCore.sol"; -import {SmartnodesToken} from "../src/SmartnodesToken.sol"; +import {SmartnodesERC20} from "../src/SmartnodesERC20.sol"; import {SmartnodesCoordinator} from "../src/SmartnodesCoordinator.sol"; import {SmartnodesDAO} from "../src/SmartnodesDAO.sol"; @@ -21,7 +21,7 @@ contract Deploy is Script { vm.startBroadcast(); - SmartnodesToken token = new SmartnodesToken( + SmartnodesERC20 token = new SmartnodesERC20( DEPLOYMENT_MULTIPLIER, genesis ); diff --git a/src/SmartnodesCoordinator.sol b/src/SmartnodesCoordinator.sol index 535a845..03f6336 100644 --- a/src/SmartnodesCoordinator.sol +++ b/src/SmartnodesCoordinator.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.22; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {ISmartnodesCore} from "./interfaces/ISmartnodesCore.sol"; -import {ISmartnodesToken} from "./interfaces/ISmartnodesToken.sol"; +import {ISmartnodesERC20} from "./interfaces/ISmartnodesERC20.sol"; /** * @title SmartnodesCoordinator @@ -31,7 +31,7 @@ contract SmartnodesCoordinator is ReentrancyGuard { // ============= State Variables ============== ISmartnodesCore private immutable i_smartnodesCore; - ISmartnodesToken private immutable i_smartnodesToken; + ISmartnodesERC20 private immutable i_smartnodesToken; uint8 private immutable i_requiredApprovalsPercentage; // Packed time-related variables @@ -134,7 +134,7 @@ contract SmartnodesCoordinator is ReentrancyGuard { } i_smartnodesCore = ISmartnodesCore(_smartnodesCore); - i_smartnodesToken = ISmartnodesToken(_smartnodesToken); + i_smartnodesToken = ISmartnodesERC20(_smartnodesToken); i_requiredApprovalsPercentage = _requiredApprovalsPercentage; timeConfig = TimeConfig({ diff --git a/src/SmartnodesCore.sol b/src/SmartnodesCore.sol index 312b0fb..d9ba784 100644 --- a/src/SmartnodesCore.sol +++ b/src/SmartnodesCore.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.22; import {ISmartnodesCoordinator} from "./interfaces/ISmartnodesCoordinator.sol"; -import {ISmartnodesToken, PaymentAmounts} from "./interfaces/ISmartnodesToken.sol"; +import {ISmartnodesERC20, PaymentAmounts} from "./interfaces/ISmartnodesERC20.sol"; /** * @title SmartnodesCore - Job Management System for Secure, Incentivised, Multi-Network P2P Resource Sharing @@ -55,7 +55,7 @@ contract SmartnodesCore { /** Constants */ uint24 private constant UNLOCK_PERIOD = 14 days; - ISmartnodesToken private immutable i_tokenContract; + ISmartnodesERC20 private immutable i_tokenContract; /** State Variables */ ISmartnodesCoordinator private validatorContract; @@ -83,7 +83,7 @@ contract SmartnodesCore { } constructor(address _tokenContract) { - i_tokenContract = ISmartnodesToken(_tokenContract); + i_tokenContract = ISmartnodesERC20(_tokenContract); jobCounter = 0; } diff --git a/src/SmartnodesDAO.sol b/src/SmartnodesDAO.sol index 65a3fd2..e91a61e 100644 --- a/src/SmartnodesDAO.sol +++ b/src/SmartnodesDAO.sol @@ -67,6 +67,7 @@ contract SmartnodesDAO is ReentrancyGuard { bool queued; address[] targets; bytes[] calldatas; + uint256[] values; string description; } @@ -137,6 +138,7 @@ contract SmartnodesDAO is ReentrancyGuard { function propose( address[] calldata targets, bytes[] calldata calldatas, + uint256[] calldata values, string calldata description ) external returns (uint256) { uint256 targetsLength = targets.length; @@ -168,13 +170,9 @@ contract SmartnodesDAO is ReentrancyGuard { p.startTime = uint128(block.timestamp); p.endTime = uint128(block.timestamp + votingPeriod); - // Copy arrays - p.targets = new address[](targetsLength); - p.calldatas = new bytes[](targetsLength); - for (uint256 i = 0; i < targetsLength; ++i) { - p.targets[i] = targets[i]; - p.calldatas[i] = calldatas[i]; - } + p.targets = targets; + p.calldatas = calldatas; + p.values = values; p.description = description; emit ProposalCreated( @@ -269,9 +267,10 @@ contract SmartnodesDAO is ReentrancyGuard { // Execute all calls uint256 targetsLength = p.targets.length; for (uint256 i = 0; i < targetsLength; ++i) { - (bool success, bytes memory returnData) = p.targets[i].call( - p.calldatas[i] - ); + (bool success, bytes memory returnData) = p.targets[i].call{ + value: p.values[i] + }(p.calldatas[i]); + if (!success) { // Handle revert reason if (returnData.length > 0) { diff --git a/src/SmartnodesToken.sol b/src/SmartnodesERC20.sol similarity index 99% rename from src/SmartnodesToken.sol rename to src/SmartnodesERC20.sol index cb4dc5a..9631500 100644 --- a/src/SmartnodesToken.sol +++ b/src/SmartnodesERC20.sol @@ -19,7 +19,7 @@ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol * @dev Uses simple DAO-based access control system to control staking requirements and upgrades * @dev to SmartnodesCore and SmartnodesCoordinator. */ -contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { +contract SmartnodesERC20 is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { /** Errors */ error Token__InsufficientBalance(); error Token__InvalidAddress(); diff --git a/src/interfaces/ISmartnodesToken.sol b/src/interfaces/ISmartnodesERC20.sol similarity index 95% rename from src/interfaces/ISmartnodesToken.sol rename to src/interfaces/ISmartnodesERC20.sol index 2ae24f1..5cf2808 100644 --- a/src/interfaces/ISmartnodesToken.sol +++ b/src/interfaces/ISmartnodesERC20.sol @@ -7,10 +7,10 @@ struct PaymentAmounts { } /** - * @title ISmartnodesToken Interface + * @title ISmartnodesERC20 Interface * @dev Interface for the SmartnodesToken contract */ -interface ISmartnodesToken { +interface ISmartnodesERC20 { function setValidatorLockAmount(uint256 _newAmount) external; function setUserLockAmount(uint256 _newAmount) external; diff --git a/test/BaseTest.sol b/test/BaseTest.sol index 2e37dab..19a1a2f 100644 --- a/test/BaseTest.sol +++ b/test/BaseTest.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.22; import {Test, console} from "forge-std/Test.sol"; -import {SmartnodesToken} from "../src/SmartnodesToken.sol"; +import {SmartnodesERC20} from "../src/SmartnodesERC20.sol"; import {SmartnodesCore} from "../src/SmartnodesCore.sol"; import {SmartnodesCoordinator} from "../src/SmartnodesCoordinator.sol"; import {SmartnodesDAO} from "../src/SmartnodesDAO.sol"; @@ -34,7 +34,7 @@ abstract contract BaseSmartnodesTest is Test { } // Contract instances - SmartnodesToken public token; + SmartnodesERC20 public token; SmartnodesCore public core; SmartnodesCoordinator public coordinator; SmartnodesDAO public dao; @@ -80,7 +80,7 @@ abstract contract BaseSmartnodesTest is Test { // genesisNodes.push(worker2); // genesisNodes.push(worker3); - token = new SmartnodesToken(DEPLOYMENT_MULTIPLIER, genesisNodes); + token = new SmartnodesERC20(DEPLOYMENT_MULTIPLIER, genesisNodes); dao = new SmartnodesDAO(address(token), DAO_VOTING_PERIOD, 500); core = new SmartnodesCore(address(token)); @@ -123,9 +123,10 @@ abstract contract BaseSmartnodesTest is Test { function createDAOProposal( address[] memory targets, bytes[] memory calldatas, + uint256[] memory values, string memory description ) internal returns (uint256 proposalId) { - proposalId = dao.propose(targets, calldatas, description); + proposalId = dao.propose(targets, calldatas, values, description); } // Helper function to vote on DAO proposals in tests diff --git a/test/CoordinatorTest.t.sol b/test/CoordinatorTest.t.sol index dab8e20..6ff3b10 100644 --- a/test/CoordinatorTest.t.sol +++ b/test/CoordinatorTest.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.22; import {Test, console} from "forge-std/Test.sol"; import {SmartnodesCoordinator} from "../src/SmartnodesCoordinator.sol"; -import {SmartnodesToken} from "../src/SmartnodesToken.sol"; +import {SmartnodesERC20} from "../src/SmartnodesERC20.sol"; import {BaseSmartnodesTest} from "./BaseTest.sol"; /** @@ -90,7 +90,7 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { ( bytes32 storedRoot, - SmartnodesToken.PaymentAmounts memory workerReward, + SmartnodesERC20.PaymentAmounts memory workerReward, uint256 storedCapacity, bool active, uint256 timestamp @@ -192,7 +192,7 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { ( bytes32 storedRoot, - SmartnodesToken.PaymentAmounts memory workerReward, + SmartnodesERC20.PaymentAmounts memory workerReward, uint256 storedCapacity, bool active, uint256 timestamp diff --git a/test/DAOTest.sol b/test/DAOTest.sol index 6f2ed01..c9d0274 100644 --- a/test/DAOTest.sol +++ b/test/DAOTest.sol @@ -10,9 +10,17 @@ import {console} from "forge-std/Test.sol"; * @notice Test contract for DAO governance functionality */ contract DAOTest is BaseSmartnodesTest { + address public projectAddress1; + address public projectAddress2; + address public projectAddress3; + function setUp() public override { super.setUp(); + projectAddress1 = makeAddr("project1"); + projectAddress2 = makeAddr("project2"); + projectAddress3 = makeAddr("project3"); + // Debug: Check token balances after setup console.log("Validator1 balance:", token.balanceOf(validator1) / 1e18); console.log("Validator2 balance:", token.balanceOf(validator2) / 1e18); @@ -21,6 +29,8 @@ contract DAOTest is BaseSmartnodesTest { console.log("Quorum required:", dao.quorumRequired() / 1e18); } + // ====== Functionality ====== + /** * @notice Test DAO proposal to set validator lock amount */ @@ -41,10 +51,14 @@ contract DAOTest is BaseSmartnodesTest { newLockAmount ); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId = createDAOProposal( targets, calldatas, + values, "Update validator lock amount to 2M SNO" ); @@ -96,10 +110,14 @@ contract DAOTest is BaseSmartnodesTest { newLockAmount ); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId = createDAOProposal( targets, calldatas, + values, "Update user lock amount to 200 SNO" ); @@ -143,10 +161,14 @@ contract DAOTest is BaseSmartnodesTest { bytes[] memory calldatas = new bytes[](1); calldatas[0] = abi.encodeWithSignature("halveDistributionInterval()"); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId = createDAOProposal( targets, calldatas, + values, "Halve the distribution interval" ); @@ -191,10 +213,14 @@ contract DAOTest is BaseSmartnodesTest { bytes[] memory calldatas = new bytes[](1); calldatas[0] = abi.encodeWithSignature("doubleDistributionInterval()"); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId = createDAOProposal( targets, calldatas, + values, "Double the distribution interval" ); @@ -235,6 +261,143 @@ contract DAOTest is BaseSmartnodesTest { console.log("Successfully doubled distribution interval via DAO"); } + /** + * @notice Test DAO proposal to fund a single project with SNO tokens + */ + function testDAOFundProjectWithSNO() public { + uint256 fundingAmount = 50_000e18; // 50k SNO tokens + + console.log("=== Testing Single SNO Project Funding ==="); + console.log("Project address:", projectAddress1); + console.log("Funding amount:", fundingAmount / 1e18, "SNO"); + + // Create proposal to transfer SNO tokens to project + address[] memory targets = new address[](1); + targets[0] = address(token); + + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = abi.encodeWithSignature( + "transfer(address,uint256)", + projectAddress1, + fundingAmount + ); + + uint256[] memory values = new uint256[](1); + values[0] = 0; + + vm.prank(validator1); + uint256 proposalId = createDAOProposal( + targets, + calldatas, + values, + "Fund Project 1 with 50k SNO tokens for development" + ); + + // Vote on proposal with sufficient votes for quorum + uint256 voteAmount = 100_000e18; + voteOnProposal(proposalId, validator1, voteAmount, true); + voteOnProposal(proposalId, validator2, voteAmount, true); + voteOnProposal(proposalId, validator3, voteAmount, true); + voteOnProposal(proposalId, user1, voteAmount, true); + voteOnProposal(proposalId, user2, voteAmount, true); + + // Check votes + (uint256 forVotes, uint256 againstVotes, uint256 totalVotes) = dao + .getProposalVotes(proposalId); + console.log("For votes:", forVotes / 1e18); + console.log("Against votes:", againstVotes / 1e18); + console.log("Total votes:", totalVotes / 1e18); + console.log("Quorum required:", dao.quorumRequired() / 1e18); + + // Record balances before execution + uint256 daoBalanceBefore = token.balanceOf(address(dao)); + uint256 projectBalanceBefore = token.balanceOf(projectAddress1); + + // Execute proposal + executeProposal(proposalId); + + // Verify transfers + uint256 daoBalanceAfter = token.balanceOf(address(dao)); + uint256 projectBalanceAfter = token.balanceOf(projectAddress1); + + assertEq( + daoBalanceAfter, + daoBalanceBefore - fundingAmount, + "DAO balance incorrect" + ); + assertEq( + projectBalanceAfter, + projectBalanceBefore + fundingAmount, + "Project balance incorrect" + ); + + console.log("Successfully funded project with SNO tokens"); + console.log("DAO balance after:", daoBalanceAfter / 1e18); + console.log("Project balance after:", projectBalanceAfter / 1e18); + } + + /** + * @notice Test DAO proposal to fund a project with ETH + */ + function testDAOFundProjectWithETH() public { + uint256 fundingAmount = 2 ether; + + console.log("=== Testing Single ETH Project Funding ==="); + console.log("Project address:", projectAddress2); + console.log("Funding amount:", fundingAmount / 1e18, "ETH"); + + // Create proposal to transfer ETH to project + address[] memory targets = new address[](1); + targets[0] = projectAddress2; + + bytes[] memory calldatas = new bytes[](1); + calldatas[0] = ""; // Empty calldata for simple ETH transfer + + uint256[] memory values = new uint256[](1); + values[0] = fundingAmount; + + vm.prank(validator1); + uint256 proposalId = createDAOProposal( + targets, + calldatas, + values, + "Fund Project 2 with 2 ETH for infrastructure" + ); + + // Vote on proposal + uint256 voteAmount = 100_000e18; + voteOnProposal(proposalId, validator1, voteAmount, true); + voteOnProposal(proposalId, validator2, voteAmount, true); + voteOnProposal(proposalId, validator3, voteAmount, true); + voteOnProposal(proposalId, user1, voteAmount, true); + voteOnProposal(proposalId, user2, voteAmount, true); + + // Record balances before execution + uint256 daoEthBefore = address(dao).balance; + uint256 projectEthBefore = address(projectAddress2).balance; + + console.log("DAO ETH before:", daoEthBefore / 1e18); + console.log("Project ETH before:", projectEthBefore / 1e18); + + // Wait for voting period to end + vm.warp(block.timestamp + DAO_VOTING_PERIOD + 1); + + // Queue the proposal + dao.queue(proposalId); + + // Wait for timelock delay + vm.warp(block.timestamp + dao.TIMELOCK_DELAY()); + + // For this test, we'll use a low-level call approach + // In practice, you'd want to add a helper function to the DAO + vm.expectRevert(); // This will fail because DAO can't send ETH with empty calldata + dao.execute(proposalId); + + console.log("ETH transfer failed as expected (need helper function)"); + } + + // ====== Logistic Checks ====== + /** * @notice Test DAO proposal failure due to insufficient votes */ @@ -251,10 +414,14 @@ contract DAOTest is BaseSmartnodesTest { newLockAmount ); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId = createDAOProposal( targets, calldatas, + values, "This proposal should fail" ); @@ -297,10 +464,14 @@ contract DAOTest is BaseSmartnodesTest { newLockAmount ); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId = createDAOProposal( targets, calldatas, + values, "This proposal should be rejected" ); @@ -350,10 +521,14 @@ contract DAOTest is BaseSmartnodesTest { newLockAmount ); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId = createDAOProposal( targets, calldatas, + values, "Test refund mechanism" ); @@ -406,10 +581,14 @@ contract DAOTest is BaseSmartnodesTest { 2_000_000e18 ); + uint256[] memory values = new uint256[](1); + values[0] = 0; + vm.prank(validator1); uint256 proposalId1 = createDAOProposal( targets1, calldatas1, + values, "Proposal 1" ); @@ -426,6 +605,7 @@ contract DAOTest is BaseSmartnodesTest { uint256 proposalId2 = createDAOProposal( targets2, calldatas2, + values, "Proposal 2" ); diff --git a/test/TokenTest.t.sol b/test/TokenTest.t.sol index b58c4c5..3978e84 100644 --- a/test/TokenTest.t.sol +++ b/test/TokenTest.t.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.22; import {console} from "forge-std/Test.sol"; -import {SmartnodesToken} from "../src/SmartnodesToken.sol"; +import {SmartnodesERC20} from "../src/SmartnodesERC20.sol"; import {BaseSmartnodesTest} from "./BaseTest.sol"; /** * @title SmartnodesTokenTest - * @notice Comprehensive tests for SmartnodesToken contract functionality + * @notice Comprehensive tests for SmartnodesERC20 contract functionality */ contract SmartnodesTokenTest is BaseSmartnodesTest { function _setupInitialState() internal override { @@ -55,20 +55,20 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { assertEq(token.balanceOf(user3), 0, "user3 should start with 0 tokens"); // User 3 doesnt have any tokens, should revert - vm.expectRevert(SmartnodesToken.Token__InsufficientBalance.selector); + vm.expectRevert(SmartnodesERC20.Token__InsufficientBalance.selector); vm.prank(address(core)); token.lockTokens(user3, false); } function testCannotLockAlreadyLockedTokens() public { vm.startPrank(address(core)); - vm.expectRevert(SmartnodesToken.Token__AlreadyLocked.selector); + vm.expectRevert(SmartnodesERC20.Token__AlreadyLocked.selector); token.lockTokens(user1, true); vm.stopPrank(); } function testCannotLockFromNonCore() public { - vm.expectRevert(SmartnodesToken.Token__InvalidAddress.selector); + vm.expectRevert(SmartnodesERC20.Token__InvalidAddress.selector); vm.prank(validator1); token.lockTokens(validator2, true); } @@ -120,14 +120,14 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { token.unlockTokens(validator1); // Try to complete unlock before period - vm.expectRevert(SmartnodesToken.Token__UnlockPending.selector); + vm.expectRevert(SmartnodesERC20.Token__UnlockPending.selector); token.unlockTokens(validator1); vm.stopPrank(); } function testCannotUnlockNeverLocked() public { vm.prank(address(core)); - vm.expectRevert(SmartnodesToken.Token__NotLocked.selector); + vm.expectRevert(SmartnodesERC20.Token__NotLocked.selector); token.unlockTokens(user3); } @@ -152,7 +152,7 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { initialContractBalance + paymentAmount ); - SmartnodesToken.PaymentAmounts memory escrowed = token + SmartnodesERC20.PaymentAmounts memory escrowed = token .getEscrowedPayments(user1); assertEq(escrowed.sno, paymentAmount); } @@ -164,7 +164,7 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { vm.prank(address(core)); token.escrowEthPayment{value: paymentAmount}(user1, paymentAmount); - SmartnodesToken.PaymentAmounts memory escrowed = token + SmartnodesERC20.PaymentAmounts memory escrowed = token .getEscrowedPayments(user1); assertEq(escrowed.eth, paymentAmount); assertEq(address(token).balance, paymentAmount); @@ -184,7 +184,7 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { vm.prank(address(core)); token.releaseEscrowedPayment(user1, paymentAmount); - SmartnodesToken.PaymentAmounts memory escrowed = token + SmartnodesERC20.PaymentAmounts memory escrowed = token .getEscrowedPayments(user1); assertEq(escrowed.sno, 0); } @@ -201,7 +201,7 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { vm.prank(address(core)); token.releaseEscrowedEthPayment(user1, paymentAmount); - SmartnodesToken.PaymentAmounts memory escrowed = token + SmartnodesERC20.PaymentAmounts memory escrowed = token .getEscrowedPayments(user1); assertEq(escrowed.eth, 0); assertEq(address(token).balance, paymentAmount); // ETH stays in contract @@ -291,7 +291,7 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { ); // Get stored reward amounts - (, SmartnodesToken.PaymentAmounts memory workerReward, , , ) = token + (, SmartnodesERC20.PaymentAmounts memory workerReward, , , ) = token .s_distributions(distributionId); // Calculate expected totals @@ -336,15 +336,15 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { // ============= Access Control Tests ============= function testOnlyCoreCanCallProtectedFunctions() public { - vm.expectRevert(SmartnodesToken.Token__InvalidAddress.selector); + vm.expectRevert(SmartnodesERC20.Token__InvalidAddress.selector); vm.prank(validator1); token.lockTokens(validator2, true); - vm.expectRevert(SmartnodesToken.Token__InvalidAddress.selector); + vm.expectRevert(SmartnodesERC20.Token__InvalidAddress.selector); vm.prank(validator1); token.unlockTokens(validator1); - vm.expectRevert(SmartnodesToken.Token__InvalidAddress.selector); + vm.expectRevert(SmartnodesERC20.Token__InvalidAddress.selector); vm.prank(validator1); token.escrowPayment(user1, 1000e18); } @@ -368,8 +368,8 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { console.log("Merkle root:", vm.toString(merkleRoot)); // Create distribution - SmartnodesToken.PaymentAmounts - memory additionalPayments = SmartnodesToken.PaymentAmounts({ + SmartnodesERC20.PaymentAmounts + memory additionalPayments = SmartnodesERC20.PaymentAmounts({ sno: uint128(ADDITIONAL_SNO_PAYMENT), eth: uint128(ADDITIONAL_ETH_PAYMENT) }); @@ -399,7 +399,7 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { // Validate distribution storage ( bytes32 storedRoot, - SmartnodesToken.PaymentAmounts memory workerReward, + SmartnodesERC20.PaymentAmounts memory workerReward, uint256 storedCapacity, bool active, uint256 timestamp @@ -418,7 +418,7 @@ contract SmartnodesTokenTest is BaseSmartnodesTest { * @param distributionId The distribution to validate */ function _validateRewardCalculations(uint256 distributionId) internal view { - (, SmartnodesToken.PaymentAmounts memory workerReward, , , ) = token + (, SmartnodesERC20.PaymentAmounts memory workerReward, , , ) = token .s_distributions(distributionId); uint256 totalSnoReward = INITIAL_EMISSION_RATE * From 3e407062ab22e787f630c11408cc250b33aa1d5c Mon Sep 17 00:00:00 2001 From: mattjhawken Date: Thu, 18 Sep 2025 15:15:21 -0400 Subject: [PATCH 4/5] Removed .DS_Store --- .DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.DS_Store b/.DS_Store index cdc8eda77d0d917ed493938a3994ae7aca486ea1..4bad658a48d0e0105429578e14a5a7d67776e8ed 100644 GIT binary patch delta 192 zcmZoMXfc=|&e%RNQH+&?fq{WzVxovF6OaJ{EI`c2z#zcDkiwA6kjPNXP?AzSF;P7R zB*Dp$#8AYL4-`g{OwLKl&(8trWd-XDT;llesy1woEd0OAB727}FxB8Qm)Qn4qR delta 109 zcmZoMXfc=|&Zs)EP}qc#fq{XUA)ld?p(Ld^IVUMUKL;cP224;IBml$$3{d%vAH~=w jF;3dd&LP0TsJijvcjn3bB8q|_WeG@H8a6wM9A*Xpj6@XH From 565e450f6c1b3237ce511f7cb597dd1e5a4b0d5d Mon Sep 17 00:00:00 2001 From: Matthew Hawken <85070849+mattjhawken@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:15:51 -0400 Subject: [PATCH 5/5] Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 4bad658a48d0e0105429578e14a5a7d67776e8ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%TB{U3>?!U6>;g2v(pR zImBxKvO9G5z!t!Uu855;ho7o0pPJcJAQeajQh`+9|0=+qt+u&!%$N$K0;#~a z0{VX_bj2DtIohv-!A1b0UgOR9Y_kNhXo6S+Cr4&z;#8tjB}NQ!I`bv!YT)GPbVv*z z5+_ScC}O8`|6=8k>X(z_zeX<0fyNlO#lD@