Skip to content

SwanDebate: Minor contract fixes #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 22, 2025
Merged
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
21 changes: 21 additions & 0 deletions deployments/abis/SwanDebate.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@
"type": "uint256",
"internalType": "uint256"
},
{
"name": "agent1ProposalId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "agent2ProposalId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "currentRound",
"type": "uint256",
Expand Down Expand Up @@ -740,6 +750,17 @@
}
]
},
{
"type": "error",
"name": "InvalidProposalCount",
"inputs": [
{
"name": "count",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "OwnableInvalidOwner",
Expand Down
45 changes: 15 additions & 30 deletions src/SwanDebate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ interface IJokeRaceContest {

/// @notice Returns vote counts for a proposal
/// @param proposalId The ID of the proposal to query
/// @return forVotes Number of votes in favor
/// @return againstVotes Number of votes against
function proposalVotes(uint256 proposalId) external view returns (uint256 forVotes, uint256 againstVotes);
/// @return votes Number of votes
function proposalVotes(uint256 proposalId) external view returns (uint256 votes);

/// @notice Returns all proposal IDs in the contest
/// @return Array of proposal IDs
Expand All @@ -46,14 +45,6 @@ interface IJokeRaceContest {
external
view
returns (address author, bool exists, string memory description);

/// @notice Sorts proposals based on votes and marks tied proposals
/// @dev Needed before retrieving the winning proposal
function setSortedAndTiedProposals() external;

/// @notice Returns sorted proposal IDs based on votes
/// @return Array of proposal IDs sorted from lowest to highest votes
function sortedProposalIds() external view returns (uint256[] memory);
}

/// @title SwanDebate
Expand Down Expand Up @@ -240,9 +231,6 @@ contract SwanDebate is Ownable, Pausable {
if (debate.currentRound != 0) revert DebateAlreadyExists(_contest);

IJokeRaceContest contest = IJokeRaceContest(_contest);
if (contest.state() != IJokeRaceContest.ContestState.Queued) {
revert ContestInvalidState(contest.state(), IJokeRaceContest.ContestState.Queued);
}

uint256[] memory proposalIds = contest.getAllProposalIds();
if (proposalIds.length != 2) {
Expand Down Expand Up @@ -335,27 +323,24 @@ contract SwanDebate is Ownable, Pausable {
/// @dev Requires contest state to be `Completed`, sorts proposals, and fetches the winner
/// @param _contest Address of the JokeRace contest
function terminateDebate(address _contest) external onlyOwner whenNotPaused {
Debate storage debate = debates[_contest];
IJokeRaceContest contest = IJokeRaceContest(_contest);
require(contest.state() == IJokeRaceContest.ContestState.Completed, "Contest not finished");

// Sort proposals based on votes before retrieving the winner
contest.setSortedAndTiedProposals();
uint256[] memory sortedProposals = contest.sortedProposalIds();

require(sortedProposals.length > 0, "No proposals found");
uint256 agent1ProposalVotes = contest.proposalVotes(debate.agent1ProposalId);
uint256 agent2ProposalVotes = contest.proposalVotes(debate.agent2ProposalId);

// The last proposal in the sorted array has the highest votes
uint256 winningProposal = sortedProposals[sortedProposals.length - 1];

uint256 winnerId = (debates[_contest].agent1ProposalId == winningProposal)
? debates[_contest].agent1Id
: debates[_contest].agent2Id;
uint256 winnerId;
if (agent1ProposalVotes >= agent2ProposalVotes) {
winnerId = debate.agent1Id;
} else {
winnerId = debate.agent2Id;
}

// Store the winner
debates[_contest].winnerId = winnerId;
debate.winnerId = winnerId;

(uint256 forVotes,) = contest.proposalVotes(winningProposal);
emit DebateTerminated(_contest, winnerId, forVotes);
emit DebateTerminated(
_contest, winnerId, agent1ProposalVotes >= agent2ProposalVotes ? agent1ProposalVotes : agent2ProposalVotes
);
}

/*//////////////////////////////////////////////////////////////
Expand Down
65 changes: 12 additions & 53 deletions test/SwanDebateTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -146,38 +146,35 @@ contract SwanDebateTest is Helper {
bytes memory input = bytes("Test input");
bytes memory models = bytes("gpt-4");

// First agent
// First agent submits output
setupOracleOutput(contestAddr, agent1Id, input, models, oracleParameters);
vm.warp(block.timestamp + 1);

// Second agent
// Second agent submits output
setupOracleOutput(contestAddr, agent2Id, input, models, oracleParameters);
vm.warp(block.timestamp + 1);

SwanDebate.RoundData memory roundData = debate.getRoundForDebate(contestAddr, 1);
assertTrue(roundData.roundComplete, "Round should be complete");

// Assign votes correctly
jokeRace.setProposalVotes(1, 100);
jokeRace.setProposalVotes(2, 50);
// Assign votes directly to the agent's proposals
(,, uint256 agent1ProposalId, uint256 agent2ProposalId,,) = debate.debates(contestAddr);

jokeRace.setState(IJokeRaceContest.ContestState.Completed);
jokeRace.setProposalVotes(agent1ProposalId, 100);
jokeRace.setProposalVotes(agent2ProposalId, 50);

// Ensure sorted proposals are set before determining the winner
jokeRace.setSortedAndTiedProposals();
jokeRace.setState(IJokeRaceContest.ContestState.Completed);

// Terminate debate and determine winner
debate.terminateDebate(contestAddr);

// Fetch sorted proposals to get the actual winner
uint256[] memory sortedProposals = jokeRace.sortedProposalIds();
uint256 winningProposal = sortedProposals[sortedProposals.length - 1];
// Fetch stored winner ID directly from the debate struct
(,,, uint256 winnerId) = debate.getDebateInfo(contestAddr);

uint256[] memory proposalIds = jokeRace.getAllProposalIds();
uint256 expectedWinner = (proposalIds[0] == winningProposal) ? agent1Id : agent2Id;
// Determine expected winner based on votes
uint256 expectedWinner = (100 > 50) ? agent1Id : agent2Id;

(,,, uint256 winnerId) = debate.getDebateInfo(contestAddr);
assertEq(winnerId, expectedWinner, "Winner should be correctly determined from sorted proposals");
assertEq(winnerId, expectedWinner, "Winner should be correctly determined based on proposal votes");
}

function test_ConcurrentDebates() external {
Expand All @@ -200,26 +197,6 @@ contract SwanDebateTest is Helper {
assertEq(agent1Debates.length, 3, "Should track all debates");
}

function test_RevertScenarios() external {
// Test multiple revert cases
vm.expectRevert(abi.encodeWithSelector(SwanDebate.AgentNotRegistered.selector));
debate.initializeDebate(0, 2, address(jokeRace));

uint256 agent1Id = debate.registerAgent();
uint256 agent2Id = debate.registerAgent();

// Wrong contest state
jokeRace.setState(IJokeRaceContest.ContestState.Active);
vm.expectRevert(
abi.encodeWithSelector(
SwanDebate.ContestInvalidState.selector,
IJokeRaceContest.ContestState.Active,
IJokeRaceContest.ContestState.Queued
)
);
debate.initializeDebate(agent1Id, agent2Id, address(jokeRace));
}

function test_ViewFunctions() external {
(address contestAddr, uint256 agent1Id,) = setupDebate();

Expand Down Expand Up @@ -285,24 +262,6 @@ contract SwanDebateTest is Helper {
debate.initializeDebate(invalidAgentId, validAgentId, address(newContest));
}

function test_EdgeCase_InvalidContestState() public {
(, uint256 agent1Id, uint256 agent2Id) = setupDebate();

MockJokeRaceContest newContest = new MockJokeRaceContest();
newContest.setState(IJokeRaceContest.ContestState.Active);
newContest.setProposalAuthor(1, address(this));
newContest.setProposalAuthor(2, address(this));

vm.expectRevert(
abi.encodeWithSelector(
SwanDebate.ContestInvalidState.selector,
IJokeRaceContest.ContestState.Active,
IJokeRaceContest.ContestState.Queued
)
);
debate.initializeDebate(agent1Id, agent2Id, address(newContest));
}

function test_EdgeCase_DuplicateDebate() public {
(address contestAddr, uint256 agent1Id, uint256 agent2Id) = setupDebate();

Expand Down
28 changes: 2 additions & 26 deletions test/mock/MockJokeRaceContest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,8 @@ contract MockJokeRaceContest is IJokeRaceContest {
}
}

function proposalVotes(uint256 proposalId)
external
view
override
returns (uint256 forVotes, uint256 againstVotes)
{
return (_votes[proposalId], 0);
function proposalVotes(uint256 proposalId) external view override returns (uint256 votes) {
return (_votes[proposalId]);
}

function getAllProposalIds() external view override returns (uint256[] memory) {
Expand All @@ -63,23 +58,4 @@ contract MockJokeRaceContest is IJokeRaceContest {
}
return false;
}

/// @notice Sort proposals by votes (ascending order)
function setSortedAndTiedProposals() external override {
uint256 length = _proposalIds.length;
_sortedProposals = _proposalIds; // Copy proposal IDs

for (uint256 i = 0; i < length; i++) {
for (uint256 j = i + 1; j < length; j++) {
if (_votes[_sortedProposals[i]] > _votes[_sortedProposals[j]]) {
(_sortedProposals[i], _sortedProposals[j]) = (_sortedProposals[j], _sortedProposals[i]);
}
}
}
}

/// @notice Returns sorted proposal IDs based on votes
function sortedProposalIds() external view override returns (uint256[] memory) {
return _sortedProposals;
}
}
Loading