diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 3f59ec8..0000000 Binary files a/.DS_Store and /dev/null differ 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 8cc12c2..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({ @@ -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/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 86% rename from src/SmartnodesToken.sol rename to src/SmartnodesERC20.sol index c39501c..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(); @@ -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(); @@ -72,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; @@ -92,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; @@ -121,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(); } } @@ -163,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); @@ -277,50 +282,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 pool to be split equally among validators + uint256 snoPool = uint256(_validatorReward.sno); + uint256 ethPool = uint256(_validatorReward.eth); - // Remaining 90% to be split equally among all validators - uint256 snoRemainingPool = uint256(_validatorReward.sno) - - snoBiasAmount; - uint256 ethRemainingPool = uint256(_validatorReward.eth) - - ethBiasAmount; - - 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,19 +315,13 @@ 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; } - _payValidator(validator, snoShare, ethShare); + _payAccount(validator, snoShare, ethShare); } emit ValidatorRewardsDistributed( @@ -359,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 @@ -388,21 +367,20 @@ 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 - * @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. + * @param _dustValidator Address of validator to receive dust rewards (usually the proposal executor) + * @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, 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({ @@ -417,59 +395,76 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { uint256 distributionId = ++s_currentDistributionId; - if (_totalCapacity == 0) { - // All rewards go to validators - _distributeValidatorRewards( - _approvedValidators, - totalReward, - distributionId, - _biasValidator - ); - 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, distributionId, - _biasValidator + _dustValidator ); } @@ -812,7 +807,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 +891,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 +917,6 @@ contract SmartnodesToken is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard { return balance; } - // ============ Required Overrides ============ - /** * @dev Override required by Solidity for multiple inheritance */ 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 5e2b72d..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"; @@ -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; @@ -33,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; @@ -79,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)); @@ -122,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 bba09b1..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"; /** @@ -51,7 +51,6 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { bytes32 proposalHash = keccak256( abi.encode( - 1, merkleRoot, validatorsToRemove, jobHashes, @@ -91,7 +90,7 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { ( bytes32 storedRoot, - SmartnodesToken.PaymentAmounts memory workerReward, + SmartnodesERC20.PaymentAmounts memory workerReward, uint256 storedCapacity, bool active, uint256 timestamp @@ -148,7 +147,6 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { bytes32 proposalHash = keccak256( abi.encode( - 1, merkleRoot, validatorsToRemove, jobHashes, @@ -194,7 +192,7 @@ contract SmartnodesCoordinatorTest is BaseSmartnodesTest { ( bytes32 storedRoot, - SmartnodesToken.PaymentAmounts memory workerReward, + SmartnodesERC20.PaymentAmounts memory workerReward, uint256 storedCapacity, bool active, uint256 timestamp @@ -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); - } } 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 24ee746..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 * @@ -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;