Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
- name: Build contracts (forge build)
run: forge build
- name: Run Foundry tests (forge test)
run: forge test --no-match-path test/invariant --no-match-path test/fuzz
run: forge test --no-match-path "test/{invariant,fuzz}/**"
- name: Run Hardhat tests
run: npx hardhat test

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/fuzz-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
forge test --match-contract StakingFuzzTest --fuzz-runs 1000 -vv

- name: Generate coverage report
run: forge coverage --report lcov
run: forge coverage --ir-minimum --report lcov

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ node_modules
# Gas reporter files
.gas-reports.json

# Foundry files
/out
/foundry.lock
lcov.info

# Hide CI workflow and automation scripts from contributors
.github/workflows/ci.yml
create_issues_contract.sh
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
59 changes: 54 additions & 5 deletions contracts/TruthBounty.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ contract TruthBountyToken is ERC20, ResolverRoleTimelock, Initializable, UUPSUpg
function setSettlementContract(address _settlement) external onlyRole(ADMIN_ROLE) {
address oldSettlement = settlementContract;
settlementContract = _settlement;
// Automatically grant RESOLVER_ROLE to the settlement contract
_grantRole(RESOLVER_ROLE, _settlement);
// Schedule RESOLVER_ROLE grant to the settlement contract (timelocked)
_scheduleResolverRoleGrant(_settlement);
emit SettlementContractUpdated(oldSettlement, _settlement);
}

Expand Down Expand Up @@ -349,8 +349,11 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna
require(claim.totalStakeAmount > 0, "No votes cast");

claim.settled = true;
bool passed = _determineOutcome(claim.totalStakedFor, claim.totalStakedAgainst);
(uint256 rewardAmount, uint256 slashedAmount) = _calculateSettlement(claimId, passed);

// Exact ties are resolved as a refund-only outcome (no rewards or slashing)
bool isTie = claim.totalStakedFor == claim.totalStakedAgainst && claim.totalStakedFor > 0;
bool passed = isTie ? false : _determineOutcome(claim.totalStakedFor, claim.totalStakedAgainst);
(uint256 rewardAmount, uint256 slashedAmount) = _calculateSettlement(claimId, passed, isTie);

emit ClaimSettled(claimId, passed, claim.totalStakedFor, claim.totalStakedAgainst, rewardAmount, slashedAmount);
}
Expand All @@ -362,8 +365,20 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna
return forPercent >= settlementThresholdPercent;
}

function _calculateSettlement(uint256 claimId, bool passed) internal returns (uint256 rewardAmount, uint256 slashedAmount) {
function _calculateSettlement(uint256 claimId, bool passed, bool isTie) internal returns (uint256 rewardAmount, uint256 slashedAmount) {
Claim storage claim = claims[claimId];

if (isTie) {
settlementResults[claimId] = SettlementResult({
passed: false,
totalRewards: 0,
totalSlashed: 0,
winnerStake: 0,
loserStake: 0
});
return (0, 0);
}

uint256 winnerStake = passed ? claim.totalStakedFor : claim.totalStakedAgainst;
uint256 loserStake = passed ? claim.totalStakedAgainst : claim.totalStakedFor;

Expand Down Expand Up @@ -391,6 +406,23 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna
require(!v.rewardClaimed, "Rewards already claimed");

SettlementResult storage settlement = settlementResults[claimId];

// Exact ties refund the full stake to every voter
bool isTie = settlement.totalRewards == 0 &&
settlement.totalSlashed == 0 &&
settlement.winnerStake == 0 &&
settlement.loserStake == 0;

if (isTie) {
require(!v.stakeReturned, "Stake already returned");
v.rewardClaimed = true;
v.stakeReturned = true;
verifierStakes[msg.sender].activeStakes -= v.stakeAmount;
require(bountyToken.transfer(msg.sender, v.stakeAmount), "Stake transfer failed");
emit StakeWithdrawn(msg.sender, v.stakeAmount);
return;
}

require(settlement.winnerStake > 0, "No winners");
require(v.support == settlement.passed, "Not a winner");

Expand Down Expand Up @@ -418,6 +450,23 @@ contract TruthBounty is AccessControl, ReentrancyGuard, Pausable, GovernanceOwna
require(!v.stakeReturned, "Stake already returned");

SettlementResult storage settlement = settlementResults[claimId];

// Exact ties refund the full stake to every voter
bool isTie = settlement.totalRewards == 0 &&
settlement.totalSlashed == 0 &&
settlement.winnerStake == 0 &&
settlement.loserStake == 0;

if (isTie) {
require(!v.stakeReturned, "Stake already returned");
v.rewardClaimed = true;
v.stakeReturned = true;
verifierStakes[msg.sender].activeStakes -= v.stakeAmount;
require(bountyToken.transfer(msg.sender, v.stakeAmount), "Stake transfer failed");
emit StakeWithdrawn(msg.sender, v.stakeAmount);
return;
}

bool isWinner = (v.support == settlement.passed);
require(!isWinner, "Winners should use claimSettlementRewards");

Expand Down
Loading
Loading