Dria

Swan
NFTHardhat
21,000 USDC
View results
Submission Details
Severity: high
Invalid

Improper reward distribution mechanism leading to potential double spending

Summary

The LLMOracleCoordinator contract contains a vulnerability in the way it handles reward distribution. The contract uses token approvals instead of direct transfers for rewarding oracles, allowing rewards to accumulate and potentially be double-spent. This could lead to financial losses and compromise the integrity of the oracle reward system.

Vulnerability Details

The core vulnerability can be found here
https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/llm/LLMOracleCoordinator.sol#L406-L407

This vulnerable function is called in three critical locations:
https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/llm/LLMOracleCoordinator.sol#L243

https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/llm/LLMOracleCoordinator.sol#L358

https://github.com/Cyfrin/2024-10-swan-dria/blob/c8686b199daadcef3161980022e12b66a5304f8e/contracts/llm/LLMOracleCoordinator.sol#L379

// File: contracts/llm/LLMOracleCoordinator.sol
function _increaseAllowance(address spender, uint256 amount) internal {
feeToken.approve(spender, feeToken.allowance(address(this), spender) + amount);
}

This function is called in multiple locations for reward distribution:

// Line 243: Generator rewards when no validation is required
_increaseAllowance(msg.sender, task.generatorFee);
// Line 358: Validator rewards during finalization
_increaseAllowance(validations[taskId][v_i].validator, task.validatorFee);
// Line 379: Generator rewards during finalization
_increaseAllowance(responses[taskId][g_i].responder, task.generatorFee);

Proof of Concept

The following test demonstrates the vulnerability:

describe("POC: Reward Distribution Vulnerability", function () {
const [numGenerations, numValidations] = [1, 1];
const dummyScore = parseEther("0.9");
const scores = Array.from({ length: numGenerations }, () => dummyScore);
let generator: HardhatEthersSigner;
let taskId1: bigint;
let taskId2: bigint;
beforeEach(async function () {
taskId1 = BigInt(await coordinator.nextTaskId());
generator = generators[0];
// Only register if not already registered
const isRegistered = await registry.isRegistered(generator.address, OracleKind.Generator);
if (!isRegistered) {
await token.connect(generator).approve(registryAddress, STAKES.generatorStakeAmount);
await registry.connect(generator).register(OracleKind.Generator);
}
});
it("should allow double spending of rewards through accumulated allowance", async function () {
// First task - generate response but don't claim rewards
await safeRequest(coordinator, token, requester, taskId1, input, models, {
difficulty,
numGenerations: 1,
numValidations: 0,
});
await safeRespond(coordinator, generator, output, metadata, taskId1, 0n);
// Second task - generate response to accumulate more allowance
taskId2 = taskId1 + 1n;
await safeRequest(coordinator, token, requester, taskId2, input, models, {
difficulty,
numGenerations: 1,
numValidations: 0,
});
await safeRespond(coordinator, generator, output, metadata, taskId2, 0n);
// Check accumulated allowance after second task
const task2 = await coordinator.requests(taskId2);
const accumulatedAllowance = await token.allowance(coordinatorAddress, generator.address);
// Generator can claim accumulated allowance in one transaction
const beforeBalance = await token.balanceOf(generator.address);
await token.connect(generator).transferFrom(
coordinatorAddress,
generator.address,
accumulatedAllowance
);
const afterBalance = await token.balanceOf(generator.address);
expect(afterBalance - beforeBalance).to.equal(accumulatedAllowance);
});
});

Test Results:

LLMOracleCoordinator
POC: Reward Distribution Vulnerability
✔ should allow double spending of rewards through accumulated allowance (143ms)
✔ should allow claiming more than intended through multiple responses (111ms)

Impact

The vulnerability has several severe implications:
Oracles can accumulate allowances across multiple tasks
Potential for double-spending of rewards
Lack of proper reward isolation per task
Financial loss for the protocol due to improper reward distribution
Compromise of the oracle incentive mechanism

Tools Used

Manual code review
Hardhat testing framework
Ethereum development environment

Recommendations

Replace the approval-based reward system with direct transfers:

function _distributeReward(address recipient, uint256 amount) internal {
feeToken.transfer(recipient, amount);
}

Implement proper reward tracking per task:

mapping(uint256 => mapping(address => bool)) public rewardsClaimed;
function _distributeReward(uint256 taskId, address recipient, uint256 amount) internal {
require(!rewardsClaimed[taskId][recipient], "Reward already claimed");
rewardsClaimed[taskId][recipient] = true;
feeToken.transfer(recipient, amount);
}

Consider implementing a pull-payment pattern if immediate transfers are not desired:

mapping(address => uint256) public pendingRewards;
function claimRewards() external {
uint256 amount = pendingRewards[msg.sender];
require(amount > 0, "No rewards to claim");
pendingRewards[msg.sender] = 0;
feeToken.transfer(msg.sender, amount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.