On June 5th, 20M OP token was stolen. This amount of OP was supposed to send to Wintermute by Optimism Foundation, however, Wintermute provided the address of their multisig on Ethereum, which they did not control on Optimism.
Before that, Wintermute confirmed that they received two test transactions with 1OP and 1M OP from Optimism. Thus, the remaining 19M OP were sent to Wintermute immediately after two test transactions on May 27th. Wintermute then realized they provided the address to which they don't have ownership.
Although Wintermute lost 20M OP, Optimism continued to launch OP token on May 31st and decided to send an additional 20M OP to Wintermute as a loan.
Was Wintermute over-optimistic?
The address corresponding to their multisig address on Ethereum had no contract deployed on Optimism, and nobody took control of that address as an EOA. Taking control of this address would require a private key.
Wintermute contacted the Gnosis Safe team and requested their assistance in recovering the funds. After consulting with the Optimism and Gnosis Safe team, Wintermute believed that no one other than Wintermute could collect these funds, so they scheduled the remediation for June 7th. The assumption, however, was proved wrong. This exploit happened due to the carelessness and optimism of Wintermute. They should have responded quickly to recover these funds.
Technical analysis of the exploit
To understand this exploit, we need to know how a smart contract is deployed. If contract Y is deployed from address X (contract or account), the address of contract Y will be calculated based on this formula:
address_Y = keccak256(address_X, nonce_Y)
nonce_Y is the number of transaction addresses X executed. In other words, we can predict the address of any contract Y deployed from address X based on X's address and transaction count of X. This value is increased after each transaction of address X. For easy understanding, if you want to deploy two smart contracts with the same address, you need to deploy contracts from the same address with the same nonce value.
As I mentioned, Wintermute's multisig address on Optimism had no contract deployed, and no one took control of this address. Finding the private key to this address is mostly impossible. Therefore, another way to take control of this address is deploying the contract on this address, for example, the Gnosis Safe proxy.
Back to 2020, on Ethereum, Wintermute multisig was created at this transaction, their multisig address is 0x4f3a120e72c76c22ae802d129f599bfdbc31cb81, for simple, we will call this address Y1. This multisig address created from Gnosis Safe Proxy Factory has address 0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B which will be called X1. Like the above formula, you can understand that Y1 is calculated from the X1 and the nonce value.
On Optimism, Gnosis Safe Proxy Factory has the same address 0x76E2cFc1F5Fa8F6a5b3fC4c8F4788F0116861F9B, we will call this X2. To take control of address 0x4f3a120e72c76c22ae802d129f599bfdbc31cb81 on Optimism, we need to create Gnosis Safe proxy from X2 with the same nonce as while creating Y1 at the time two years ago. This nonce value will be called nonce_Y1. Since the trading volume on Optimism is smaller than Ethereum, right before the exploit, the nonce of Gnosis Safe Proxy Factory on Optimism is less than nonce_Y1.
So now, hackers need to repeatedly deploy multisig with function createProxy() from Gnosis Safe Proxy Factory to increase the nonce value until it reaches nonce_Y1. The hacker used this contract to execute the exploit, this contract ran batch deployments of 162 multisigs per transaction. The hacker achieved 0x4f3a120e72c76c22ae802d129f599bfdbc31cb81 at this transaction in log event 135. More than 60 transactions were executed, and the hacker only took 1-2 minutes to achieve this address.
Demo this exploit on Kovan testnet
Remix IDE will be used to demo this exploit in a simple way. Since Optimistic has layer two on the Kovan testnet, we will demo the attack on this testnet. To prepare for this exploit, I have already deployed Gnosis Safe Proxy Factory and Master Copy on Kovan Test Network and Optimistic Ethereum Testnet:
- Kovan Test Network:
- Gnosis Safe Proxy Factory: 0x469BCe20B9f4E69AFaB62D1EAF9b3A6c5cDfcE36
- Master Copy: 0xa8c3203243F0d792BBaC6051d5e10d19CFd73358
- Optimistic Ethereum Testnet:
- Gnosis Safe Proxy Factory: 0x469BCe20B9f4E69AFaB62D1EAF9b3A6c5cDfcE36
- Master Copy: 0xa8c3203243F0d792BBaC6051d5e10d19CFd73358
At first, we copy the code of Gnosis Safe Proxy Factory from this to Remix IDE and compile it.
Second, contract ProxyFactory is loaded from this into Remix through these steps:
- Change network to Kovan Test Network
- Choose contract ProxyFactory
- Enter address of contract ProxyFactory 0x469bce20b9f4e69afab62d1eaf9b3a6c5cdfce36 to At Address and then click At Address
After loading successfully, we can interact with contract ProxyFactory:
Then, we create our multisig by function createProxy() with Master Copy 0xa8c3203243F0d792BBaC6051d5e10d19CFd73358
- Enter the address of Master Copy and data, then click transact
- Confirm transaction
After the transaction is completed, we have our multisig created at this transaction. Switch to tab log to watch our multisig address:
0x2722905bc9eef4e89db2a574256bf970a2144006 is our multisig address on Kovan testnet.
Then, on metamask, we swap to Optimistic Ethereum Testnet and send 5000 DAI to this address:
After the transaction, on Optimistic testnet, this multisig has the balance of 5000 DAI:
However, 0x2722905bc9eef4e89db2a574256bf970a2144006 is our multisig on Kovan testnet, and we have not been the owner of this multisig on Optimistic Testnet.
The next step is taking control of this multisig address on the Optimistic testnet. Contract Exploiter:
GnosisSafeProxyFactory.sol is taken from https://kovan.etherscan.io/address/0x469bce20b9f4e69afab62d1eaf9b3a6c5cdfce36#code
GnosisSafeMasterCopy.sol is taken from https://kovan.etherscan.io/address/0xa8c3203243f0d792bbac6051d5e10d19cfd73358
Since the source code the hacker used to exploit is unknown, I wrote a simple contract to demo this exploit. In this contract, the function createMultipleMultisig() will be used to exploit. This function will call to function createMultisig() with the number of times is _batchSize passed in the function call. Function createMultisig() will use function createProxy() of contract ProxyFactory to create multisig. Two parameters passed to function createProxy() are the address of Master Copy and variable data. Variable data is defined as the call to function setup() in contract ProxyFactory. Function setup() will execute delegatecall to contract Exploiter with data which is variable callApprove we passed in. The variable callApprove is defined to approve contract Exploiter to spend _token of the created multisig.
We will use account 0xf5117eE57C0008ddBB22202DCe6702D2D00FBb43 as hacker account to take control of this multisig. First, we deploy the Exploiter contract by these steps:
- Choose Optimistic Ethereum Testnet on metamask
- Enter address of contract ProxyFactory: 0x469bce20b9f4e69afab62d1eaf9b3a6c5cdfce36 and then click transact
After deployment, we have a contract Exploiter. To take ownership of multisig 0x2722905bc9eef4e89db2a574256bf970a2144006, we will call function createMultipleMultisig() until we create a multisig which has address 0x2722905bc9eef4e89db2a574256bf970a2144006
- Enter address of Master Copy: 0xa8c3203243F0d792BBaC6051d5e10d19CFd73358, address of DAI token: 0xda10009cbd5d07dd0cecc66161fc93d7c9000da1, batch size and then click transact
- Confirm transaction
After 3 times call this function, we have taken control of multisig 0x2722905bc9eef4e89db2a574256bf970a2144006. We take ownership of this multisig at this transaction:
You can read more about the detail of these transactions here
Now, we can use function transfer() of contract Exploiter to transfer 5000 DAI from this multisig to hacker account 0xf5117eE57C0008ddBB22202DCe6702D2D00FBb43
- Enter address of DAI token: 0xda10009cbd5d07dd0cecc66161fc93d7c9000da1, address of multisig: 0x2722905bc9eef4e89db2a574256bf970a2144006, address of hacker account: 0xf5117eE57C0008ddBB22202DCe6702D2D00FBb43, amount: 5000 DAI, and then click transact
- Confirm transaction
Detail of transfer transaction. After transfer, the hacker account has 5000 DAI:
Conclusion
Luckily, that hacker returned 17M OP to Wintermute at the time of writing this post. The incident of Wintermute stems from their subjective judgment that could be called an "amateur mistake": from the confusion between multisig addresses ownership on layer 1 and layer 2 to the over-optimistic assessment that the funds could only be recovered by them, as well as scheduling remediation too late.
In this post, I explained the calculation mechanism of the contract address and implemented a realistic demo of the exploitation. That could help you clearly understand Wintermute's fault and the hacker's attack vector. By doing that, we hope you could gain more insights into smart contract programming and more importantly, avoid the same mistake in the future.
References
[1] Wintermute - REKT, rekt.news, accessed June 16th, 2022.
[2] Wintermute's 'Amateur Mistake' Leads to Loss of 20M Optimism Tokens, cryptonews.com, accessed June 16th, 2022.
[3] Analysis and recreation of the 20 million $OP hack on optimism chain, ChainZoom, accessed June 16th, 2022.