Recumbent Shamrock Barracuda
High
The current implementation of the voting market allows an attacker to profit by alternately buying TRUST and DISTRUST votes one at a time and then selling them in bulk. The vulnerability is due to the way _calcVotePrice recalculates the vote price during each transaction. Since the bonding curve mechanism redistributes the price based on the relative number of votes, an attacker can exploit the price difference by manipulating the market state. https://github.com/sherlock-audit/2024-11-ethos-network-ii/blob/main/ethos/packages/contracts/contracts/ReputationMarket.sol#L920
The root cause of the vulnerability lies in the design of the bonding curve mechanism and the lack of safeguards against rapid alternation between buying and selling votes. Specifically:
-
Bonding Curve Behavior:
- The
_calcVotePrice
function calculates vote prices dynamically based on the relative count ofTRUST
andDISTRUST
votes using the bonding curve formula:price = (votes * basePrice) / totalVotes;
- Each purchase or sale affects the price of both vote types (
TRUST
andDISTRUST
), creating opportunities for manipulation by alternately buying and selling to exploit the shifting price.
- The
-
No Restriction on Alternating Trades:
- The protocol does not impose restrictions on alternating between
TRUST
andDISTRUST
trades. This allows an attacker to execute rapid trades in a specific pattern to exploit the price differential caused by their actions.
- The protocol does not impose restrictions on alternating between
-
Bulk Sale Vulnerability:
- Selling votes in bulk does not fully reflect the incremental price reduction that should occur during the sale process. Instead, the system overvalues the votes being sold, leading to excessive profits for the attacker.
By leveraging these design gaps, an attacker can systematically exploit the protocol to extract profits while depleting protocol funds.
- Profit extraction: An attacker can extract profits by alternately buying and selling votes at manipulated price points.
- Funding depletion: Repeated exploitation can deplete the protocol’s funds.
- Assume that
initialVotes
are like this: market.votes[TRUST] = 1 market.votes[DISTRUST] = 1 - Attacker buys votes as follows: 1 TRUST, 1 DISTRUST, 1 TRUST, 1 DISTRUST ... =>(1 TRUST, 1 DISTRUST) * 10
- Attacker sells votes as follows: 10 TRUST, 10 DISTRUST
- The funds the attacker receives by selling are greater than the funds spent by buying and the buying fee and selling fee.
In the example, the number of initial vote was set to 1 according to test code, but this attack is possible in any case.
- Assume that
initialVotes
andmarket.basePrice
are like this: market.votes[TRUST] = 1 market.votes[DISTRUST] = 1 market.basePrice = 10000 (For convenience of calculation) - Set all feeBasePoints at max: entryProtocolFeeBasisPoints=500 exitProtocolFeeBasisPoints=500 donationBasisPoints=500
- Total funds for buying with attack path:
- buy 1 TRUST: (Since a 10% fee is taken from the funds at first to buy votes, the total funds required is equal to the vote value divided by 0.9.) votePrice = (1 * 10000) / 2 = 5000 totalFund = votePrice + protocolFee + donation = 5000 / 0.9 = 5556 As result, TRUST=2, DISTRUST=1;
- buy 1 DISTRUST: votePrice = (1 * 10000) / 3 = 3334 totalFund = 3334 / 0.9 = 3705 As result, TRUST=2, DISTRUST=2;
- buy 1 TRUST: votePrice = (2 * 10000) / 4 = 5000 totalFund = 5000 / 0.9 = 5556 As result, TRUST=3, DISTRUST=2;
- buy 1 DISTRUST: votePrice = (2 * 10000) / 5 = 4000 totalFund = 5000 / 0.9 = 4445 As result, TRUST=3, DISTRUST=3; ... Total Funds = (1 * 10000) / 2 / 0.9 + (1 * 10000) / 3 / 0.9 + (2 * 10000) / 4 / 0.9 + (2 * 10000) / 5 / 0.9 + ... + (10 * 10000) / 20 / 0.9 + (10 * 10000) / 21 / 0.9 = 104541
- Total funds from selling with attack path:
- sell 10 TRUST: (After the votePrice is calculated, a 5% fee is charged, so the funds you receive are equal to the vote value multiplied by 0.95.) votePrice = (10 * 10000) / 21 + (9 * 10000) / 20 + ... (1 * 10000) / 12 = 31192 totalFund = votePrice - protocolFee = 31192 * 0.95 = 29632 As result, TRUST= 1, DISTRUST=11;
- sell 10 DISTRUST: votePrice = (10 * 10000) / 11 + (9 * 10000) / 10 + ... (1 * 10000) / 2 = 79798 totalFund = 79798 * 0.95 = 75808 As result, TRUST= 1, DISTRUST=1; Total Funds = 29632 + 75808 = 105440
- Even though the number of votes before and after the purchase is the same, the attacker gains and the protocol loses from the difference between the funds spent on the purchase and the funds received upon the purchase(Users only need to have enough funds to buy votes).
diff = 105440 - 104541 = 899
Attacker can make a profit of around 9% by conducting a single attack with funds of around 10.5 times the
market.basePrice
.
Attacker can attack with less funds by buying and selling as follows:
1=TRUST vote, 0=DISTRUST vote (1, 1) => (4, 7)
buy: 0, 1, 0, 1, 0, 1, 0 * 3 => totalFund = [(1 * 10000) / 2 / 0.9 + (1 * 10000) / 3 / 0.9 + (2 * 10000) / 4 / 0.9 + (2 * 10000) / 5 / 0.9 + (3 * 10000) / 6 / 0.9 + (3 * 10000) / 7 / 0.9]
+ [(4 * 10000) / 8 + (5 * 10000) / 9 + (6 * 10000) / 10] / 0.9
= 47,967
sell: 1 * 3, 0 * 6 => totalFund = [(3 * 10000) / 10 + (2 * 10000) / 9 + (1 * 10000) / 8] * 0.95
+ [(6 * 10000) / 7 + (5 * 10000) / 6 + (4 * 10000) / 5 + (3 * 10000) / 4 + (2 * 10000) / 3 + (1 * 10000) / 2] * 0.95
= 48,014
diff = 48,014 - 47,967 = 47
Attacker can make a profit of around 0.5% by conducting a single attack with funds of around 4.8 times the market.basePrice
.
If the
initialVote
is 2, attacker can attack like this: (2, 2) => (9, 16) buy: (0, 1) * 7, 0 * 7 sell: 1 * 7, 0 * 14 Attacker can make a profit of around 0.8% by conducting a single attack with funds of around 11.8 times themarket.basePrice
.
If the
initialVote
is 5, attacker can attack like this: (5, 5) => (25, 45) buy: (0, 1) * 20, 0 * 20 sell: 1 * 20, 0 * 40 Attacker can make a profit of around 5.1% by conducting a single attack with funds of around 32.8 times themarket.basePrice
.
The larger the initialVote
, the more votes need to be purchased for an attack, but with enough initial funds for that, it is possible to steal all the protocol's funds by repeating the attack multiple times.
The attack is possible even if the number of TRUST and DISTRUST votes is an any value different from the initialVote
(2, 4) => (7, 15) buy: 1 * 2, (0, 1) * 3, 0 * 8 sell: 1 * 5, 0 * 11 Attacker can make a profit of around 4.8% by conducting a single attack with funds of around 9.3 times the
market.basePrice
.
- Changes the pricing logic when selling votes.: The selling price is calculated based on the state at the time the vote is sold (i.e. 1 is deducted).
function _calculateSell() {
...
--- market.votes[isPositive ? TRUST : DISTRUST] -= 1;
--- votePrice = _calcVotePrice(market, isPositive);
+++ votePrice = _calcVotePriceForSelling(market, isPositive);
+++ market.votes[isPositive ? TRUST : DISTRUST] -= 1;
...
}
function _calcVotePriceForSelling(Market memory market, bool isPositive) private pure returns (uint256) {
uint256 totalVotes = market.votes[TRUST] + market.votes[DISTRUST];
return ((market.votes[isPositive ? TRUST : DISTRUST] - 1) * (market.basePrice)) / totalVotes;
}
- Charge Fees on Both Buy and Sell: Apply a higher fee for frequent trading to discourage exploitation