Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions clear
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PID PPID PGID WINPID TTY UID STIME COMMAND
2242 1735 2242 5524 cons1 197610 14:20:53 /usr/bin/PS
1735 1 1735 4912 cons1 197610 14:06:43 /usr/bin/bash
27 changes: 21 additions & 6 deletions contracts/TruthBounty.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "@openzeppelin/contracts/access/AccessControl.sol";
import "./utils/ResolverRoleTimelock.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import "./governance/GovernanceOwnable.sol";
import "./governance/GovernanceHooks.sol";
Expand Down Expand Up @@ -167,6 +167,8 @@ contract TruthBounty is ResolverRoleTimelock, ReentrancyGuard, Pausable, Governa
uint256 totalSlashed;
uint256 winnerStake;
uint256 loserStake;
uint256 claimedWinnerStake; // Tracks claimed stake to identify last claimant
uint256 distributedRewards; // Tracks rewards already distributed to calculate remainder
}

// Verifier staking information
Expand Down Expand Up @@ -212,9 +214,6 @@ contract TruthBounty is ResolverRoleTimelock, ReentrancyGuard, Pausable, Governa
event StakeDeposited(address indexed verifier, uint256 amount);
event StakeWithdrawn(address indexed verifier, uint256 amount);
event RewardsClaimed(address indexed verifier, uint256 amount);
event ETHReceived(address indexed sender, uint256 amount);
event ETHRescued(address indexed recipient, uint256 amount);

constructor(address _bountyToken, address initialAdmin, address _governanceController) {
require(_bountyToken != address(0), "Invalid token address");
require(initialAdmin != address(0), "Invalid admin address");
Expand Down Expand Up @@ -328,7 +327,10 @@ contract TruthBounty is ResolverRoleTimelock, ReentrancyGuard, Pausable, Governa
uint256 winnerStake = passed ? claim.totalStakedFor : claim.totalStakedAgainst;
uint256 loserStake = passed ? claim.totalStakedAgainst : claim.totalStakedFor;

// Calculate slashed amount - capture remainder
slashedAmount = (loserStake * slashPercent) / 100;

// Calculate reward amount from slashed - capture remainder
rewardAmount = (slashedAmount * rewardPercent) / 100;

totalSlashed += slashedAmount;
Expand All @@ -339,7 +341,9 @@ contract TruthBounty is ResolverRoleTimelock, ReentrancyGuard, Pausable, Governa
totalRewards: rewardAmount,
totalSlashed: slashedAmount,
winnerStake: winnerStake,
loserStake: loserStake
loserStake: loserStake,
claimedWinnerStake: 0,
distributedRewards: 0
});
}

Expand All @@ -357,7 +361,18 @@ contract TruthBounty is ResolverRoleTimelock, ReentrancyGuard, Pausable, Governa
bool isWinner = (vote.support == settlement.passed);
require(isWinner, "Not a winner");

uint256 reward = (vote.stakeAmount * settlement.totalRewards) / settlement.winnerStake;
uint256 reward;

settlement.claimedWinnerStake += vote.stakeAmount;

if (settlement.claimedWinnerStake == settlement.winnerStake) {
reward = settlement.totalRewards - settlement.distributedRewards;
} else {
reward = (vote.stakeAmount * settlement.totalRewards) / settlement.winnerStake;
}

settlement.distributedRewards += reward;

vote.rewardClaimed = true;

if (reward > 0) {
Expand Down
94 changes: 50 additions & 44 deletions contracts/TruthBountyWeighted.sol
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,8 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
uint256 totalSlashed;
uint256 winnerWeightedStake; // Changed to weighted (NEW)
uint256 loserWeightedStake; // Changed to weighted (NEW)
uint256 winnerCount; // Number of winning voters eligible for rewards
uint256 winnersClaimed; // Number of winning voters that claimed rewards
uint256 rewardsClaimed; // Total rewards already distributed
uint256 claimedWinnerWeightedStake; // Tracks claimed stake to identify last claimant
uint256 distributedRewards; // Tracks rewards already distributed to calculate remainder
}

struct VerifierStake {
Expand Down Expand Up @@ -456,21 +455,30 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
require(claim.submitter != address(0), "Claim does not exist");
require(claim.settled, "Claim not settled");

Vote storage vote = votes[claimId][msg.sender];
require(vote.voted, "No vote cast");
require(!vote.rewardClaimed, "Rewards already claimed");
Vote storage voteRecord = votes[claimId][msg.sender];
require(voteRecord.voted, "No vote cast");
require(!voteRecord.rewardClaimed, "Rewards already claimed");

SettlementResult storage settlement = settlementResults[claimId];
require(settlement.winnerWeightedStake > 0, "No winners");

// Check if verifier was on the winning side
bool isWinner = (vote.support == settlement.passed);
bool isWinner = (voteRecord.support == settlement.passed);
require(isWinner, "Not a winner");

// Calculate proportional reward based on EFFECTIVE stake. Integer division can
// leave a remainder, so assign any undistributed dust to the final winning
// claimant to ensure totalRewards is fully paid out.
uint256 reward = (vote.effectiveStake * settlement.totalRewards) / settlement.winnerWeightedStake;
// Calculate proportional reward based on EFFECTIVE stake
uint256 reward;

settlement.claimedWinnerWeightedStake += voteRecord.effectiveStake;

if (settlement.claimedWinnerWeightedStake == settlement.winnerWeightedStake) {
// Last claimant gets all remaining rewards to prevent dust accumulation
reward = settlement.totalRewards - settlement.distributedRewards;
} else {
reward = (voteRecord.effectiveStake * settlement.totalRewards) / settlement.winnerWeightedStake;
}

settlement.distributedRewards += reward;

settlement.winnersClaimed += 1;
if (settlement.winnersClaimed == settlement.winnerCount) {
Expand All @@ -479,7 +487,7 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
settlement.rewardsClaimed += reward;

// Mark as claimed
vote.rewardClaimed = true;
voteRecord.rewardClaimed = true;

// Transfer reward
if (reward > 0) {
Expand All @@ -488,10 +496,10 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
}

// Return stake (winners get full RAW stake back)
if (!vote.stakeReturned) {
vote.stakeReturned = true;
verifierStakes[msg.sender].activeStakes -= vote.stakeAmount;
require(bountyToken.transfer(msg.sender, vote.stakeAmount), "Stake transfer failed");
if (!voteRecord.stakeReturned) {
voteRecord.stakeReturned = true;
verifierStakes[msg.sender].activeStakes -= voteRecord.stakeAmount;
require(bountyToken.transfer(msg.sender, voteRecord.stakeAmount), "Stake transfer failed");
}
}

Expand All @@ -504,27 +512,27 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
require(claim.submitter != address(0), "Claim does not exist");
require(claim.settled, "Claim not settled");

Vote storage vote = votes[claimId][msg.sender];
require(vote.voted, "No vote cast");
require(!vote.stakeReturned, "Stake already returned");
Vote storage voteRecord = votes[claimId][msg.sender];
require(voteRecord.voted, "No vote cast");
require(!voteRecord.stakeReturned, "Stake already returned");

SettlementResult storage settlement = settlementResults[claimId];
bool isWinner = (vote.support == settlement.passed);
bool isWinner = (voteRecord.support == settlement.passed);

uint256 stakeToReturn;
uint256 slashAmount = vote.slashAmount; // Use pre-calculated slash amount (no recalculation)
uint256 slashAmount = voteRecord.slashAmount; // Use pre-calculated slash amount (no recalculation)

if (isWinner) {
stakeToReturn = vote.stakeAmount;
stakeToReturn = voteRecord.stakeAmount;
} else {
// Losers get stake back minus slashing (pre-calculated at settlement)
stakeToReturn = vote.stakeAmount - slashAmount;
stakeToReturn = voteRecord.stakeAmount - slashAmount;

emit StakeSlashed(claimId, msg.sender, slashAmount);
}

vote.stakeReturned = true;
verifierStakes[msg.sender].activeStakes -= vote.stakeAmount;
voteRecord.stakeReturned = true;
verifierStakes[msg.sender].activeStakes -= voteRecord.stakeAmount;

if (!isWinner) {
verifierStakes[msg.sender].totalStaked -= slashAmount;
Expand All @@ -539,25 +547,25 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
* @notice Withdraw available stake (not locked in active claims)
*/
function withdrawStake(uint256 amount) external nonReentrant whenNotPaused {
VerifierStake storage stake = verifierStakes[msg.sender];
VerifierStake storage vStake = verifierStakes[msg.sender];
require(
stake.totalStaked >= stake.activeStakes + amount,
vStake.totalStaked >= vStake.activeStakes + amount,
"Insufficient available stake"
);

// If no exit has been initiated yet, start the cooldown clock
if (stake.exitTime == 0) {
stake.exitTime = block.timestamp;
if (vStake.exitTime == 0) {
vStake.exitTime = block.timestamp;
revert("Withdrawal initiated. Please wait 2 days cooldown.");
}

// Ensure the 2 days cooldown window has passed
require(block.timestamp >= stake.exitTime + 2 days, "Cooldown active");
require(block.timestamp >= vStake.exitTime + 2 days, "Cooldown active");

// Reset the exit clock for future actions
stake.exitTime = 0;
vStake.exitTime = 0;

stake.totalStaked -= amount;
vStake.totalStaked -= amount;
require(bountyToken.transfer(msg.sender, amount), "Transfer failed");

emit StakeWithdrawn(msg.sender, amount);
Expand Down Expand Up @@ -762,9 +770,8 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
totalSlashed: slashedAmount,
winnerWeightedStake: winnerWeightedStake,
loserWeightedStake: loserWeightedStake,
winnerCount: _countWinners(claimId, passed),
winnersClaimed: 0,
rewardsClaimed: 0
claimedWinnerWeightedStake: 0,
distributedRewards: 0
});
}

Expand All @@ -785,28 +792,28 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
/**
* @notice Assign per-vote slash amounts to each loser
* @dev Iterates through all voters and stores slash amount in Vote struct for losers
* @return totalSlashed Sum of all slash amounts
* @return totalSlashedAmount Sum of all slash amounts
*/
function _assignPerVoteSlashes(
uint256 claimId,
bool passed
) internal returns (uint256 totalSlashed) {
) internal returns (uint256 totalSlashedAmount) {
address[] storage voters = claimVoters[claimId];

for (uint256 i = 0; i < voters.length; i++) {
address voter = voters[i];
Vote storage vote = votes[claimId][voter];
Vote storage voteRecord = votes[claimId][voter];

bool isLoser = (vote.support != passed);
bool isLoser = (voteRecord.support != passed);

if (isLoser) {
// Calculate slash as the configured percentage of raw stake.
uint256 slashAmount = (vote.stakeAmount * slashPercent) / PERCENT_DENOMINATOR;
vote.slashAmount = slashAmount;
totalSlashed += slashAmount;
uint256 slashAmount = (voteRecord.stakeAmount * slashPercent) / PERCENT_DENOMINATOR;
voteRecord.slashAmount = slashAmount;
totalSlashedAmount += slashAmount;
} else {
// Winners are not slashed
vote.slashAmount = 0;
voteRecord.slashAmount = 0;
}
}
}
Expand Down Expand Up @@ -1070,4 +1077,3 @@ contract TruthBountyWeighted is ResolverRoleTimelock, ReentrancyGuard, Pausable,
bountyToken = IERC20(_newBountyToken);
}
}

9 changes: 2 additions & 7 deletions contracts/governance/GovernanceOwnable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,12 +230,7 @@ abstract contract GovernanceOwnable is AccessControl, Pausable {
* @notice Get governance controller interface version
* @return The version number ( 0 if no controller )
*/
function getGovernanceVersion() external view returns (uint256) {
function getGovernanceVersion() external pure returns (uint256) {
return 1; // Placeholder for future governance version tracking
}

// ============ Reserved Storage ============

/// @dev Storage gap for future upgrades (reserved 50 slots)
uint256[50] private __gap;
}
}
Empty file added forge
Empty file.
8 changes: 8 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"lib/forge-std": {
"rev": "b3bc8b154382a75d0b0ef22d7fd4a0a5f0feee0e"
},
"lib/openzeppelin-contracts": {
"rev": "74edc4baff50b93c06977021ee9ba25987803291"
}
}
8 changes: 1 addition & 7 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[profile.default]
src = "test"
src = "contracts"
out = "out"
libs = ["lib"]
test = "test"
Expand All @@ -10,12 +10,6 @@ cache_path = "cache"
optimizer = true
optimizer_runs = 200

[profile.default.model_checker]
contracts = { "contracts/WeightedStaking.sol" = [ "WeightedStaking" ], "contracts/staking.sol" = [ "Staking" ] }
engine = "chc"
timeout = 10000
targets = [ "assert" ]

[fmt]
line_length = 120
tab_width = 4
Expand Down
1 change: 1 addition & 0 deletions out/AccessControl.sol/AccessControl.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions out/Base.sol/CommonBase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"abi":[],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.35+commit.47b9dedd\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"stateVariables\":{\"CONSOLE\":{\"details\":\"console.sol and console2.sol work by executing a staticcall to this address. Calculated as `address(uint160(uint88(bytes11(\\\"console.log\\\"))))`.\"},\"CREATE2_FACTORY\":{\"details\":\"Used when deploying with create2. Taken from https://github.com/Arachnid/deterministic-deployment-proxy.\"},\"DEFAULT_SENDER\":{\"details\":\"The default address for tx.origin and msg.sender. Calculated as `address(uint160(uint256(keccak256(\\\"foundry default caller\\\"))))`.\"},\"DEFAULT_TEST_CONTRACT\":{\"details\":\"The address of the first contract `CREATE`d by a running test contract. When running tests, each test contract is `CREATE`d by `DEFAULT_SENDER` with nonce 1. Calculated as `VM.computeCreateAddress(VM.computeCreateAddress(DEFAULT_SENDER, 1), 1)`.\"},\"MULTICALL3_ADDRESS\":{\"details\":\"Deterministic deployment address of the Multicall3 contract. Taken from https://www.multicall3.com.\"},\"SECP256K1_ORDER\":{\"details\":\"The order of the secp256k1 curve.\"},\"VM_ADDRESS\":{\"details\":\"Cheat code address. Calculated as `address(uint160(uint256(keccak256(\\\"hevm cheat code\\\"))))`.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/forge-std/src/Base.sol\":\"CommonBase\"},\"evmVersion\":\"osaka\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/\",\":erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts/\"]},\"sources\":{\"lib/forge-std/src/Base.sol\":{\"keccak256\":\"0x926f1c9907b7dadb56dc920ae80dc473070989ab1f09b63e207ddc2d37110faa\",\"license\":\"MIT OR Apache-2.0\",\"urls\":[\"bzz-raw://8a470cde3eb7aa7ff3ac0d71c6b8e06f394609b937a7edbc7b5fdfab18c2b710\",\"dweb:/ipfs/QmUgmpmVzLXfmxhmnPjx9TUc2WdgNdBuucFZfh6fSciaFp\"]},\"lib/forge-std/src/StdStorage.sol\":{\"keccak256\":\"0x15972e480f9a3fc9d104a5fe981580c613fa625a5b3cd412b3540c3e927b60aa\",\"license\":\"MIT OR Apache-2.0\",\"urls\":[\"bzz-raw://cd1a221b35acd58f6d10fb19468b1281b744f3fac10097ae9c103d7ee89e0aa1\",\"dweb:/ipfs/QmbUD1p4b4Vkq6EsZqoKYnkuRETDQiJgt5KSuTvQFn3HfW\"]},\"lib/forge-std/src/Vm.sol\":{\"keccak256\":\"0x0b50ffdf244156d98d50b6b3c37cdb982edf843bba0916f09455611d9830588d\",\"license\":\"MIT OR Apache-2.0\",\"urls\":[\"bzz-raw://6133428f8cf753fab357f2f97efd474fedd9263d0b0c6c2e47363f3392e15ff4\",\"dweb:/ipfs/QmSrgd4byLu16hfUjZHwdq3zDHnCDHpXQgKSYrXQ68yybn\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.35+commit.47b9dedd"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/","openzeppelin-contracts/=lib/openzeppelin-contracts/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"lib/forge-std/src/Base.sol":"CommonBase"},"evmVersion":"osaka","libraries":{}},"sources":{"lib/forge-std/src/Base.sol":{"keccak256":"0x926f1c9907b7dadb56dc920ae80dc473070989ab1f09b63e207ddc2d37110faa","urls":["bzz-raw://8a470cde3eb7aa7ff3ac0d71c6b8e06f394609b937a7edbc7b5fdfab18c2b710","dweb:/ipfs/QmUgmpmVzLXfmxhmnPjx9TUc2WdgNdBuucFZfh6fSciaFp"],"license":"MIT OR Apache-2.0"},"lib/forge-std/src/StdStorage.sol":{"keccak256":"0x15972e480f9a3fc9d104a5fe981580c613fa625a5b3cd412b3540c3e927b60aa","urls":["bzz-raw://cd1a221b35acd58f6d10fb19468b1281b744f3fac10097ae9c103d7ee89e0aa1","dweb:/ipfs/QmbUD1p4b4Vkq6EsZqoKYnkuRETDQiJgt5KSuTvQFn3HfW"],"license":"MIT OR Apache-2.0"},"lib/forge-std/src/Vm.sol":{"keccak256":"0x0b50ffdf244156d98d50b6b3c37cdb982edf843bba0916f09455611d9830588d","urls":["bzz-raw://6133428f8cf753fab357f2f97efd474fedd9263d0b0c6c2e47363f3392e15ff4","dweb:/ipfs/QmSrgd4byLu16hfUjZHwdq3zDHnCDHpXQgKSYrXQ68yybn"],"license":"MIT OR Apache-2.0"}},"version":1},"id":7}
Loading
Loading