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
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:
_increaseAllowance(msg.sender, task.generatorFee);
_increaseAllowance(validations[taskId][v_i].validator, task.validatorFee);
_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];
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 () {
await safeRequest(coordinator, token, requester, taskId1, input, models, {
difficulty,
numGenerations: 1,
numValidations: 0,
});
await safeRespond(coordinator, generator, output, metadata, taskId1, 0n);
taskId2 = taskId1 + 1n;
await safeRequest(coordinator, token, requester, taskId2, input, models, {
difficulty,
numGenerations: 1,
numValidations: 0,
});
await safeRespond(coordinator, generator, output, metadata, taskId2, 0n);
const task2 = await coordinator.requests(taskId2);
const accumulatedAllowance = await token.allowance(coordinatorAddress, generator.address);
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);
}