├── .gitignore ├── FTXGasAbuse ├── Analysis.md ├── XENCrypto.sol └── img │ ├── claimMintRewardtx.png │ ├── claimRanktx.png │ ├── transaction.png │ └── tx2.png ├── Meebits ├── Analysis.md ├── Meebits.sol └── img │ ├── fail_brute_force_mint.png │ └── successful_brute_force_mint.png ├── MultiSig ├── Analysis.md ├── Wallet.sol └── img │ ├── attacker_txs.png │ ├── execute_tx.png │ └── init_tx.png ├── ParityBug ├── Analysis.md ├── WalletLibrary.sol └── img │ ├── github.png │ ├── initWallet.png │ └── kill.png ├── ProofofWeakHandsCoin ├── Analysis.md ├── PonziTokenV3.sol └── img │ ├── approve.png │ ├── transfer.png │ ├── transferFrom.png │ └── withdraw.png ├── README.md └── TempleDAO ├── Analysis.md ├── StaxLPStaking.sol └── img ├── contract_deployment.png ├── exploit_tx.png ├── hacker_tx_summary.png ├── stax.fi.png ├── swaps.png └── tornado.png /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /FTXGasAbuse/Analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis of the FTX Exchange Gas Abuse 2 | In October 2022, a user abused free gas, offer by the FTX exchange for their withdraw, to mint XEN token, which requires no ETH, only gas for the transaction. It costed FTX over 81 ETH in gas fee and the exploiter made over 61 ETH by swapping the obtain XEN for ETH, WBTC and USDC. 3 | 4 | ## Addresses involved 5 | The FTX Exchange address: [0xC098B2a3Aa256D2140208C3de6543aAEf5cd3A94](https://etherscan.io/address/0xC098B2a3Aa256D2140208C3de6543aAEf5cd3A94) 6 | The XEN contract: [0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8](https://etherscan.io/address/0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8#code) 7 | 8 | One of the ClaimRank tx: [0xe3bef0cb7b7c9415ed53c29b4495672ffcef318d2be22da434ed12aef1ca9f11](https://etherscan.io/tx/0xe3bef0cb7b7c9415ed53c29b4495672ffcef318d2be22da434ed12aef1ca9f11) 9 | One of the claimMintReward tx: [0xb6d42419b3d280ea0be680c0e8839bc252c877a731fba50f5b8d46cb1d8e98b3](https://etherscan.io/tx/0xb6d42419b3d280ea0be680c0e8839bc252c877a731fba50f5b8d46cb1d8e98b3) 10 | One of the swap tx: [0x199ba78683c8ede6f69c91cb6c51aaa4da4dbe7b2164c87a7a673dc860bcda50](https://etherscan.io/tx/0x199ba78683c8ede6f69c91cb6c51aaa4da4dbe7b2164c87a7a673dc860bcda50) 11 | 12 | The exploiter address: [0x1d371CF00038421d6e57CFc31EEff7A09d4B8760](https://etherscan.io/address/0x1d371CF00038421d6e57CFc31EEff7A09d4B8760) 13 | The exploit contract: [0xCba9b1Fd69626932c704DAc4CB58c29244A47FD3](https://etherscan.io/address/0xCba9b1Fd69626932c704DAc4CB58c29244A47FD3) 14 | 15 | ## The vulnerability: Gas limit too high on withdraw 16 | 17 | The FTX exchange offer free withdraw (transaction sent from the FTX address), with a transaction where the gas limit is set to 500'000. The withdraw transaction call the `fallback()` function on the address where the funds are to be sent, if the address is a contract, it will then executed whatever code is in that function until the gas runs out. With a gas limit of 500'000, there is a lot of gas left after the ETH transfer. A user can then use that gas to do anything they want. 18 | 19 | The XEN token doesn't necessary have a vulnerability. It was used as intended, except they probably didn't expect a user to have access to free gas. The users can mint token with `claimRank(uint256 term)`: 20 | ```solidity 21 | function claimRank(uint256 term) external { 22 | uint256 termSec = term * SECONDS_IN_DAY; //** minimum waiting period is a day 23 | require(termSec > MIN_TERM, "CRank: Term less than min"); 24 | require( 25 | termSec < _calculateMaxTerm() + 1, 26 | "CRank: Term more than current max term" 27 | ); 28 | require( 29 | userMints[_msgSender()].rank == 0, //** only one mint at a time 30 | "CRank: Mint already in progress" 31 | ); 32 | 33 | // create and store new MintInfo 34 | MintInfo memory mintInfo = MintInfo({ 35 | user: _msgSender(), 36 | term: term, 37 | maturityTs: block.timestamp + termSec, 38 | rank: globalRank, 39 | amplifier: _calculateRewardAmplifier(), 40 | eaaRate: _calculateEAARate() 41 | }); 42 | userMints[_msgSender()] = mintInfo;exploit 43 | activeMinters++; 44 | emit RankClaimed(_msgSender(), term, globalRank++); 45 | } 46 | ``` 47 | wait `term` days, and then the user can claim their token with `claimMintReward()` or `claimMintRewardAndShare(receiverAddr, splitPerCentage)`. Those functions are similar, except that with the latter you can send the token to another address (or split them). The exploiter will use this to sent himself the token since he will mint them with a bunch of contracts that will self-destruct: 48 | ```solidity 49 | function claimMintRewardAndShare(address other, uint256 pct) external { 50 | MintInfo memory mintInfo = userMints[_msgSender()]; 51 | require(other != address(0), "CRank: Cannot share with zero address"); 52 | require(pct > 0, "CRank: Cannot share zero percent"); 53 | require(pct < 101, "CRank: Cannot share 100+ percent"); 54 | require(mintInfo.rank > 0, "CRank: No mint exists"); 55 | require( 56 | block.timestamp > mintInfo.maturityTs, //** check waiting period 57 | "CRank: Mint maturity not reached" 58 | ); 59 | 60 | // calculate reward 61 | uint256 rewardAmount = _calculateMintReward( 62 | mintInfo.rank, 63 | mintInfo.term, 64 | mintInfo.maturityTs, 65 | mintInfo.amplifier, 66 | mintInfo.eaaRate 67 | ) * 1 ether; 68 | uint256 sharedReward = (rewardAmount * pct) / 100; 69 | uint256 ownReward = rewardAmount - sharedReward; 70 | 71 | // mint reward tokens 72 | _mint(_msgSender(), ownReward); 73 | _mint(other, sharedReward); 74 | 75 | _cleanUpUserMint(); 76 | emit MintClaimed(_msgSender(), rewardAmount); 77 | } 78 | ``` 79 | 80 | ## The exploit 81 | 82 | A user decided to use that free gas to mint some XEN. XEN is an ERC-20 token where the mint function requires no ETH just the gas needed for the transaction. 83 | The exploiter deployed an exploitContract and made the FTX exchange send it some ETH. 84 | When the FTX exchange called the exploitContract `fallback()` function to sent some ETH (0.0035ETH), the exploitContract will use as much as the 500'000 gas limit to mint XEN tokens. It first deploys a dummy contract, mint the token with `XEN.claimRank(1)` with this contract and then selfdestruct. He finally send the received ETH to the exploiterAddr. Since some gas is left, he does it again with other dummy contracts. An address can only do one mint at the time, and has to wait at least a day until it can withdraw the token to mint again. He did dozens of similar transactions. All gas fee are paid by the FTX exchange. 85 | 86 | ![claimRank transaction](img/claimRanktx.png "claimRank transaction") 87 | 88 | After a day, the tokens can be claimed, so the contracts will now call `XEN.claimMintRewardAndShare(exploiterAddr, 100)`, claiming their token and sharing them all with the exploiter address. It will then remint some token (and claim them a day later). Finally it selfdestruct the dummyContract. Since at this point there is still some unused gas, he does the same two more time with two other dummy contracts. At the end, the `fallback()` function send the received 0.0035ETH to the exploiter address. Again, since the FTX address is the sender of the transaction, so it pays all the gas fee. 89 | 90 | ![claimMintReward transaction](img/claimMintRewardtx.png "claimMintReward transaction") 91 | 92 | In one transaction, the exploiter ends up with around 171,000 XEN, the FTX address loses around 0.01 ETH in gas fee. 93 | 94 | He then started to do call his exploitContract himself and creating dozens of dummyContracts and netting over 5MM of XEN per transaction. 95 | 96 | ![transaction 2](img/tx2.png "transaction 2") 97 | 98 | 99 | ## The aftermath 100 | 101 | The exploiter costed FTX over 81 ETH in transactions fee and earned over 61 ETH by swapping the XEN token for ETH, WBTC and USDC. -------------------------------------------------------------------------------- /FTXGasAbuse/XENCrypto.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.10; 3 | 4 | import "./Math.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/interfaces/IERC165.sol"; 7 | import "abdk-libraries-solidity/ABDKMath64x64.sol"; 8 | import "./interfaces/IStakingToken.sol"; 9 | import "./interfaces/IRankedMintingToken.sol"; 10 | import "./interfaces/IBurnableToken.sol"; 11 | import "./interfaces/IBurnRedeemable.sol"; 12 | 13 | contract XENCrypto is 14 | Context, 15 | IRankedMintingToken, 16 | IStakingToken, 17 | IBurnableToken, 18 | ERC20("XEN Crypto", "XEN") 19 | { 20 | using Math for uint256; 21 | using ABDKMath64x64 for int128; 22 | using ABDKMath64x64 for uint256; 23 | 24 | // INTERNAL TYPE TO DESCRIBE A XEN MINT INFO 25 | struct MintInfo { 26 | address user; 27 | uint256 term; 28 | uint256 maturityTs; 29 | uint256 rank; 30 | uint256 amplifier; 31 | uint256 eaaRate; 32 | } 33 | 34 | // INTERNAL TYPE TO DESCRIBE A XEN STAKE 35 | struct StakeInfo { 36 | uint256 term; 37 | uint256 maturityTs; 38 | uint256 amount; 39 | uint256 apy; 40 | } 41 | 42 | // PUBLIC CONSTANTS 43 | 44 | uint256 public constant SECONDS_IN_DAY = 3_600 * 24; 45 | uint256 public constant DAYS_IN_YEAR = 365; 46 | 47 | uint256 public constant GENESIS_RANK = 1; 48 | 49 | uint256 public constant MIN_TERM = 1 * SECONDS_IN_DAY - 1; 50 | uint256 public constant MAX_TERM_START = 100 * SECONDS_IN_DAY; 51 | uint256 public constant MAX_TERM_END = 1_000 * SECONDS_IN_DAY; 52 | uint256 public constant TERM_AMPLIFIER = 15; 53 | uint256 public constant TERM_AMPLIFIER_THRESHOLD = 5_000; 54 | uint256 public constant REWARD_AMPLIFIER_START = 3_000; 55 | uint256 public constant REWARD_AMPLIFIER_END = 1; 56 | uint256 public constant EAA_PM_START = 100; 57 | uint256 public constant EAA_PM_STEP = 1; 58 | uint256 public constant EAA_RANK_STEP = 100_000; 59 | uint256 public constant WITHDRAWAL_WINDOW_DAYS = 7; 60 | uint256 public constant MAX_PENALTY_PCT = 99; 61 | 62 | uint256 public constant XEN_MIN_STAKE = 0; 63 | 64 | uint256 public constant XEN_MIN_BURN = 0; 65 | 66 | uint256 public constant XEN_APY_START = 20; 67 | uint256 public constant XEN_APY_DAYS_STEP = 90; 68 | uint256 public constant XEN_APY_END = 2; 69 | 70 | string public constant AUTHORS = "@MrJackLevin @lbelyaev faircrypto.org"; 71 | 72 | // PUBLIC STATE, READABLE VIA NAMESAKE GETTERS 73 | 74 | uint256 public immutable genesisTs; 75 | uint256 public globalRank = GENESIS_RANK; 76 | uint256 public activeMinters; 77 | uint256 public activeStakes; 78 | uint256 public totalXenStaked; 79 | // user address => XEN mint info 80 | mapping(address => MintInfo) public userMints; 81 | // user address => XEN stake info 82 | mapping(address => StakeInfo) public userStakes; 83 | // user address => XEN burn amount 84 | mapping(address => uint256) public userBurns; 85 | 86 | // CONSTRUCTOR 87 | constructor() { 88 | genesisTs = block.timestamp; 89 | } 90 | 91 | // PRIVATE METHODS 92 | 93 | /** 94 | * @dev calculates current MaxTerm based on Global Rank 95 | * (if Global Rank crosses over TERM_AMPLIFIER_THRESHOLD) 96 | */ 97 | function _calculateMaxTerm() private view returns (uint256) { 98 | if (globalRank > TERM_AMPLIFIER_THRESHOLD) { 99 | uint256 delta = globalRank 100 | .fromUInt() 101 | .log_2() 102 | .mul(TERM_AMPLIFIER.fromUInt()) 103 | .toUInt(); 104 | uint256 newMax = MAX_TERM_START + delta * SECONDS_IN_DAY; 105 | return Math.min(newMax, MAX_TERM_END); 106 | } 107 | return MAX_TERM_START; 108 | } 109 | 110 | /** 111 | * @dev calculates Withdrawal Penalty depending on lateness 112 | */ 113 | function _penalty(uint256 secsLate) private pure returns (uint256) { 114 | // =MIN(2^(daysLate+3)/window-1,99) 115 | uint256 daysLate = secsLate / SECONDS_IN_DAY; 116 | if (daysLate > WITHDRAWAL_WINDOW_DAYS - 1) return MAX_PENALTY_PCT; 117 | uint256 penalty = (uint256(1) << (daysLate + 3)) / 118 | WITHDRAWAL_WINDOW_DAYS - 119 | 1; 120 | return Math.min(penalty, MAX_PENALTY_PCT); 121 | } 122 | 123 | /** 124 | * @dev calculates net Mint Reward (adjusted for Penalty) 125 | */ 126 | function _calculateMintReward( 127 | uint256 cRank, 128 | uint256 term, 129 | uint256 maturityTs, 130 | uint256 amplifier, 131 | uint256 eeaRate 132 | ) private view returns (uint256) { 133 | uint256 secsLate = block.timestamp - maturityTs; 134 | uint256 penalty = _penalty(secsLate); 135 | uint256 rankDelta = Math.max(globalRank - cRank, 2); 136 | uint256 EAA = (1_000 + eeaRate); 137 | uint256 reward = getGrossReward(rankDelta, amplifier, term, EAA); 138 | return (reward * (100 - penalty)) / 100; 139 | } 140 | 141 | /** 142 | * @dev cleans up User Mint storage (gets some Gas credit;)) 143 | */ 144 | function _cleanUpUserMint() private { 145 | delete userMints[_msgSender()]; 146 | activeMinters--; 147 | } 148 | 149 | /** 150 | * @dev calculates XEN Stake Reward 151 | */ 152 | function _calculateStakeReward( 153 | uint256 amount, 154 | uint256 term, 155 | uint256 maturityTs, 156 | uint256 apy 157 | ) private view returns (uint256) { 158 | if (block.timestamp > maturityTs) { 159 | uint256 rate = (apy * term * 1_000_000) / DAYS_IN_YEAR; 160 | return (amount * rate) / 100_000_000; 161 | } 162 | return 0; 163 | } 164 | 165 | /** 166 | * @dev calculates Reward Amplifier 167 | */ 168 | function _calculateRewardAmplifier() private view returns (uint256) { 169 | uint256 amplifierDecrease = (block.timestamp - genesisTs) / 170 | SECONDS_IN_DAY; 171 | if (amplifierDecrease < REWARD_AMPLIFIER_START) { 172 | return 173 | Math.max( 174 | REWARD_AMPLIFIER_START - amplifierDecrease, 175 | REWARD_AMPLIFIER_END 176 | ); 177 | } else { 178 | return REWARD_AMPLIFIER_END; 179 | } 180 | } 181 | 182 | /** 183 | * @dev calculates Early Adopter Amplifier Rate (in 1/000ths) 184 | * actual EAA is (1_000 + EAAR) / 1_000 185 | */ 186 | function _calculateEAARate() private view returns (uint256) { 187 | uint256 decrease = (EAA_PM_STEP * globalRank) / EAA_RANK_STEP; 188 | if (decrease > EAA_PM_START) return 0; 189 | return EAA_PM_START - decrease; 190 | } 191 | 192 | /** 193 | * @dev calculates APY (in %) 194 | */ 195 | function _calculateAPY() private view returns (uint256) { 196 | uint256 decrease = (block.timestamp - genesisTs) / 197 | (SECONDS_IN_DAY * XEN_APY_DAYS_STEP); 198 | if (XEN_APY_START - XEN_APY_END < decrease) return XEN_APY_END; 199 | return XEN_APY_START - decrease; 200 | } 201 | 202 | /** 203 | * @dev creates User Stake 204 | */ 205 | function _createStake(uint256 amount, uint256 term) private { 206 | userStakes[_msgSender()] = StakeInfo({ 207 | term: term, 208 | maturityTs: block.timestamp + term * SECONDS_IN_DAY, 209 | amount: amount, 210 | apy: _calculateAPY() 211 | }); 212 | activeStakes++; 213 | totalXenStaked += amount; 214 | } 215 | 216 | // PUBLIC CONVENIENCE GETTERS 217 | 218 | /** 219 | * @dev calculates gross Mint Reward 220 | */ 221 | function getGrossReward( 222 | uint256 rankDelta, 223 | uint256 amplifier, 224 | uint256 term, 225 | uint256 eaa 226 | ) public pure returns (uint256) { 227 | int128 log128 = rankDelta.fromUInt().log_2(); 228 | int128 reward128 = log128 229 | .mul(amplifier.fromUInt()) 230 | .mul(term.fromUInt()) 231 | .mul(eaa.fromUInt()); 232 | return reward128.div(uint256(1_000).fromUInt()).toUInt(); 233 | } 234 | 235 | /** 236 | * @dev returns User Mint object associated with User account address 237 | */ 238 | function getUserMint() external view returns (MintInfo memory) { 239 | return userMints[_msgSender()]; 240 | } 241 | 242 | /** 243 | * @dev returns XEN Stake object associated with User account address 244 | */ 245 | function getUserStake() external view returns (StakeInfo memory) { 246 | return userStakes[_msgSender()]; 247 | } 248 | 249 | /** 250 | * @dev returns current AMP 251 | */ 252 | function getCurrentAMP() external view returns (uint256) { 253 | return _calculateRewardAmplifier(); 254 | } 255 | 256 | /** 257 | * @dev returns current EAA Rate 258 | */ 259 | function getCurrentEAAR() external view returns (uint256) { 260 | return _calculateEAARate(); 261 | } 262 | 263 | /** 264 | * @dev returns current APY 265 | */ 266 | function getCurrentAPY() external view returns (uint256) { 267 | return _calculateAPY(); 268 | } 269 | 270 | /** 271 | * @dev returns current MaxTerm 272 | */ 273 | function getCurrentMaxTerm() external view returns (uint256) { 274 | return _calculateMaxTerm(); 275 | } 276 | 277 | // PUBLIC STATE-CHANGING METHODS 278 | 279 | /** 280 | * @dev accepts User cRank claim provided all checks pass (incl. no current claim exists) 281 | */ 282 | function claimRank(uint256 term) external { 283 | uint256 termSec = term * SECONDS_IN_DAY; 284 | require(termSec > MIN_TERM, "CRank: Term less than min"); 285 | require( 286 | termSec < _calculateMaxTerm() + 1, 287 | "CRank: Term more than current max term" 288 | ); 289 | require( 290 | userMints[_msgSender()].rank == 0, 291 | "CRank: Mint already in progress" 292 | ); 293 | 294 | // create and store new MintInfo 295 | MintInfo memory mintInfo = MintInfo({ 296 | user: _msgSender(), 297 | term: term, 298 | maturityTs: block.timestamp + termSec, 299 | rank: globalRank, 300 | amplifier: _calculateRewardAmplifier(), 301 | eaaRate: _calculateEAARate() 302 | }); 303 | userMints[_msgSender()] = mintInfo; 304 | activeMinters++; 305 | emit RankClaimed(_msgSender(), term, globalRank++); 306 | } 307 | 308 | /** 309 | * @dev ends minting upon maturity (and within permitted Withdrawal Time Window), gets minted XEN 310 | */ 311 | function claimMintReward() external { 312 | MintInfo memory mintInfo = userMints[_msgSender()]; 313 | require(mintInfo.rank > 0, "CRank: No mint exists"); 314 | require( 315 | block.timestamp > mintInfo.maturityTs, 316 | "CRank: Mint maturity not reached" 317 | ); 318 | 319 | // calculate reward and mint tokens 320 | uint256 rewardAmount = _calculateMintReward( 321 | mintInfo.rank, 322 | mintInfo.term, 323 | mintInfo.maturityTs, 324 | mintInfo.amplifier, 325 | mintInfo.eaaRate 326 | ) * 1 ether; 327 | _mint(_msgSender(), rewardAmount); 328 | 329 | _cleanUpUserMint(); 330 | emit MintClaimed(_msgSender(), rewardAmount); 331 | } 332 | 333 | /** 334 | * @dev ends minting upon maturity (and within permitted Withdrawal time Window) 335 | * mints XEN coins and splits them between User and designated other address 336 | */ 337 | function claimMintRewardAndShare(address other, uint256 pct) external { 338 | MintInfo memory mintInfo = userMints[_msgSender()]; 339 | require(other != address(0), "CRank: Cannot share with zero address"); 340 | require(pct > 0, "CRank: Cannot share zero percent"); 341 | require(pct < 101, "CRank: Cannot share 100+ percent"); 342 | require(mintInfo.rank > 0, "CRank: No mint exists"); 343 | require( 344 | block.timestamp > mintInfo.maturityTs, 345 | "CRank: Mint maturity not reached" 346 | ); 347 | 348 | // calculate reward 349 | uint256 rewardAmount = _calculateMintReward( 350 | mintInfo.rank, 351 | mintInfo.term, 352 | mintInfo.maturityTs, 353 | mintInfo.amplifier, 354 | mintInfo.eaaRate 355 | ) * 1 ether; 356 | uint256 sharedReward = (rewardAmount * pct) / 100; 357 | uint256 ownReward = rewardAmount - sharedReward; 358 | 359 | // mint reward tokens 360 | _mint(_msgSender(), ownReward); 361 | _mint(other, sharedReward); 362 | 363 | _cleanUpUserMint(); 364 | emit MintClaimed(_msgSender(), rewardAmount); 365 | } 366 | 367 | /** 368 | * @dev ends minting upon maturity (and within permitted Withdrawal time Window) 369 | * mints XEN coins and stakes 'pct' of it for 'term' 370 | */ 371 | function claimMintRewardAndStake(uint256 pct, uint256 term) external { 372 | MintInfo memory mintInfo = userMints[_msgSender()]; 373 | // require(pct > 0, "CRank: Cannot share zero percent"); 374 | require(pct < 101, "CRank: Cannot share >100 percent"); 375 | require(mintInfo.rank > 0, "CRank: No mint exists"); 376 | require( 377 | block.timestamp > mintInfo.maturityTs, 378 | "CRank: Mint maturity not reached" 379 | ); 380 | 381 | // calculate reward 382 | uint256 rewardAmount = _calculateMintReward( 383 | mintInfo.rank, 384 | mintInfo.term, 385 | mintInfo.maturityTs, 386 | mintInfo.amplifier, 387 | mintInfo.eaaRate 388 | ) * 1 ether; 389 | uint256 stakedReward = (rewardAmount * pct) / 100; 390 | uint256 ownReward = rewardAmount - stakedReward; 391 | 392 | // mint reward tokens part 393 | _mint(_msgSender(), ownReward); 394 | _cleanUpUserMint(); 395 | emit MintClaimed(_msgSender(), rewardAmount); 396 | 397 | // nothing to burn since we haven't minted this part yet 398 | // stake extra tokens part 399 | require(stakedReward > XEN_MIN_STAKE, "XEN: Below min stake"); 400 | require(term * SECONDS_IN_DAY > MIN_TERM, "XEN: Below min stake term"); 401 | require( 402 | term * SECONDS_IN_DAY < MAX_TERM_END + 1, 403 | "XEN: Above max stake term" 404 | ); 405 | require(userStakes[_msgSender()].amount == 0, "XEN: stake exists"); 406 | 407 | _createStake(stakedReward, term); 408 | emit Staked(_msgSender(), stakedReward, term); 409 | } 410 | 411 | /** 412 | * @dev initiates XEN Stake in amount for a term (days) 413 | */ 414 | function stake(uint256 amount, uint256 term) external { 415 | require(balanceOf(_msgSender()) >= amount, "XEN: not enough balance"); 416 | require(amount > XEN_MIN_STAKE, "XEN: Below min stake"); 417 | require(term * SECONDS_IN_DAY > MIN_TERM, "XEN: Below min stake term"); 418 | require( 419 | term * SECONDS_IN_DAY < MAX_TERM_END + 1, 420 | "XEN: Above max stake term" 421 | ); 422 | require(userStakes[_msgSender()].amount == 0, "XEN: stake exists"); 423 | 424 | // burn staked XEN 425 | _burn(_msgSender(), amount); 426 | // create XEN Stake 427 | _createStake(amount, term); 428 | emit Staked(_msgSender(), amount, term); 429 | } 430 | 431 | /** 432 | * @dev ends XEN Stake and gets reward if the Stake is mature 433 | */ 434 | function withdraw() external { 435 | StakeInfo memory userStake = userStakes[_msgSender()]; 436 | require(userStake.amount > 0, "XEN: no stake exists"); 437 | 438 | uint256 xenReward = _calculateStakeReward( 439 | userStake.amount, 440 | userStake.term, 441 | userStake.maturityTs, 442 | userStake.apy 443 | ); 444 | activeStakes--; 445 | totalXenStaked -= userStake.amount; 446 | 447 | // mint staked XEN (+ reward) 448 | _mint(_msgSender(), userStake.amount + xenReward); 449 | emit Withdrawn(_msgSender(), userStake.amount, xenReward); 450 | delete userStakes[_msgSender()]; 451 | } 452 | 453 | /** 454 | * @dev burns XEN tokens and creates Proof-Of-Burn record to be used by connected DeFi services 455 | */ 456 | function burn(address user, uint256 amount) public { 457 | require(amount > XEN_MIN_BURN, "Burn: Below min limit"); 458 | require( 459 | IERC165(_msgSender()).supportsInterface( 460 | type(IBurnRedeemable).interfaceId 461 | ), 462 | "Burn: not a supported contract" 463 | ); 464 | 465 | _spendAllowance(user, _msgSender(), amount); 466 | _burn(user, amount); 467 | userBurns[user] += amount; 468 | IBurnRedeemable(_msgSender()).onTokenBurned(user, amount); 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /FTXGasAbuse/img/claimMintRewardtx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/FTXGasAbuse/img/claimMintRewardtx.png -------------------------------------------------------------------------------- /FTXGasAbuse/img/claimRanktx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/FTXGasAbuse/img/claimRanktx.png -------------------------------------------------------------------------------- /FTXGasAbuse/img/transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/FTXGasAbuse/img/transaction.png -------------------------------------------------------------------------------- /FTXGasAbuse/img/tx2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/FTXGasAbuse/img/tx2.png -------------------------------------------------------------------------------- /Meebits/Analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis of the Meebits exploit 2 | 3 | In May 2021, an attacker, armed with the list of rare Meebits tokenIds, managed to brute force the early mint of the NFT and revert all transactions except the ones that would mint a rare NFT. He managed to mint one before the mint was paused, and sold it for 200 ETH. 4 | 5 | The exploited Meebits contract on etherscan: [0x7bd29408f11d2bfc23c34f18275bbf23bb716bc7](https://etherscan.io/address/0x7bd29408f11d2bfc23c34f18275bbf23bb716bc7#code) 6 | The exploit transaction hash: [0xcad228421360736da7c5a07ae0bdfc868c6a66613109f64a24ccffedfdd5f04d](https://etherscan.io/tx/0xcad228421360736da7c5a07ae0bdfc868c6a66613109f64a24ccffedfdd5f04d) 7 | The transaction hash with the sale: [0x8edd496c28603b334a57dbf459b3d1fc61a33b08e8aaaaf7f634080482c3f026](https://etherscan.io/tx/0x8edd496c28603b334a57dbf459b3d1fc61a33b08e8aaaaf7f634080482c3f026) 8 | 9 | The hacker address: [0xb08be767cdc33913f8e2fa44193f4e2eb5725876](https://etherscan.io/address/0xb08be767cdc33913f8e2fa44193f4e2eb5725876) 10 | The hacker address 2: [0x009988ff77eeaa00051238ee32c48f10a174933e](https://etherscan.io/address/0x009988ff77eeaa00051238ee32c48f10a174933e) 11 | The Exploit contract: [0x270ff2308a29099744230de56e7b41c8ced46ffb](https://etherscan.io/address/0x270ff2308a29099744230de56e7b41c8ced46ffb) 12 | 13 | ## The vulnerability: leaked rarity traits 14 | 15 | Inside a NFT collection, some token are more rare than others, and thus more valuable. The rarity is baseed on the token's traits. Unfortunately, for the Meebits NFT, the contract contained a file with the metadata of the collection, containing the traits and their rarity for each tokenId. So when you minted an NFT, you could know its relative value. 16 | 17 | ```solidity 18 | // IPFS Hash to the NFT content 19 | string public contentHash = 20 | "QmfXYgfX1qNfzQ6NRyFnupniZusasFPMeiWn5aaDnx7YXo"; 21 | ``` 22 | 23 | Since the `mint()` function returns the id of the minted token, a contract could easily call the `mint()` function and `revert` if the returned id was not in a list of rare tokenId. 24 | ```solidity 25 | 26 | /** 27 | * Public sale minting. 28 | */ 29 | function mint() external payable reentrancyGuard returns (uint256) { 30 | require(publicSale, "Sale not started."); 31 | require(!marketPaused); 32 | require(numSales < SALE_LIMIT, "Sale limit reached."); 33 | uint256 salePrice = getPrice(); 34 | require(msg.value >= salePrice, "Insufficient funds to purchase."); 35 | if (msg.value > salePrice) { 36 | msg.sender.transfer(msg.value.sub(salePrice)); 37 | } 38 | beneficiary.transfer(salePrice); 39 | numSales++; 40 | return _mint(msg.sender, 0); 41 | } 42 | function _mint(address _to, uint256 createdVia) internal returns (uint256) { 43 | require(_to != address(0), "Cannot mint to 0x0."); 44 | require(numTokens < TOKEN_LIMIT, "Token limit reached."); 45 | uint256 id = randomIndex(); 46 | 47 | numTokens = numTokens + 1; 48 | _addNFToken(_to, id); 49 | 50 | emit Mint(id, _to, createdVia); 51 | emit Transfer(address(0), _to, id); 52 | return id; 53 | } 54 | ``` 55 | 56 | ## The exploit 57 | 58 | The attacker deployed his attack contract, with a list of rare tokenId, and an `attack()` function which simply called `mintWithPunkOrGlyph()` and made sure the minted token was rare, otherwise the tx would revert. It also sent 1 ETH to the miner to ensure that the transaction would be added to the block: 59 | 60 | ```solidity 61 | function attack(uint _punkId) public{ 62 | uint256 tokenId = meebits.mintWithPunkOrGlyph(_punkId); // mint a meebits 63 | require(rareMeebits[tokenId]); //check if its a rare one, revert if not 64 | block.coinbase.call{value: 1 ether}(""); // pay the miner 65 | } 66 | ``` 67 | Before the public mint of the Meebits NFT, they allowed anyone with a CryptoPunks NFT (or an Autoglyph) to mint a Meebits (one per Punks/Glyph NFT). This is why the attacker called `mintWithPunkOrGlyph()` and not `mint()`. In order to be able to call this function, the attacker purchased a cryptoPunk, and sent it to the attack contract. 68 | The `mintWithPunkOrGlyph()` function: 69 | ```solidity 70 | /** 71 | * Community grant minting. 72 | */ 73 | function mintWithPunkOrGlyph(uint256 _createVia) 74 | external 75 | reentrancyGuard 76 | returns (uint256) 77 | { 78 | require(communityGrant); 79 | require(!marketPaused); 80 | require( 81 | _createVia > 0 && _createVia <= 10512, 82 | "Invalid punk/glyph index." 83 | ); 84 | require( 85 | creatorNftMints[_createVia] == 0, 86 | "Already minted with this punk/glyph" 87 | ); 88 | if (_createVia > 10000) { 89 | ... // It's a glyph 90 | } else { 91 | // It's a punk 92 | // Compute the punk ID 93 | uint256 punkId = _createVia.sub(1); 94 | // Make sure the sender owns the punk 95 | require( 96 | Cryptopunks(punks).punkIndexToAddress(punkId) == msg.sender, 97 | "Not the owner of this punk." 98 | ); 99 | } 100 | creatorNftMints[_createVia]++; 101 | return _mint(msg.sender, _createVia); 102 | } 103 | 104 | function _mint(address _to, uint256 createdVia) internal returns (uint256) { 105 | require(_to != address(0), "Cannot mint to 0x0."); 106 | require(numTokens < TOKEN_LIMIT, "Token limit reached."); 107 | uint256 id = randomIndex(); 108 | 109 | numTokens = numTokens + 1; 110 | _addNFToken(_to, id); 111 | 112 | emit Mint(id, _to, createdVia); 113 | emit Transfer(address(0), _to, id); 114 | return id; 115 | } 116 | ``` 117 | He then start spaming the `attack()` function. This would obvioulsy fail a lot: 118 | 119 | ![Failed mint transaction](img/fail_brute_force_mint.png "Failed mint transaction") 120 | 121 | 122 | until one succeeded less than an hour later: 123 | 124 | ![Successful mint transaction](img/successful_brute_force_mint.png "Successful mint transaction") 125 | 126 | He then quickly sold Meebits #16647 for 200 ETH and bought another Punk to try again. 127 | 128 | 129 | ## The Aftermath 130 | 131 | Fortunately, the Meebits team was reactive and quickly stopped the mint, less than 3 hours after the brute forcing started. The attacker only managed to mint one rare token. 132 | 133 | 134 | -------------------------------------------------------------------------------- /Meebits/Meebits.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2021-05-05 3 | */ 4 | 5 | pragma solidity 0.7.6; 6 | 7 | /** 8 | * __ __ _ _ _ 9 | * | \/ | | | (_) | 10 | * | \ / | ___ ___| |__ _| |_ ___ 11 | * | |\/| |/ _ \/ _ \ '_ \| | __/ __| 12 | * | | | | __/ __/ |_) | | |_\__ \ 13 | * |_| |_|\___|\___|_.__/|_|\__|___/ 14 | * 15 | * An NFT project from Larva Labs. 16 | * 17 | */ 18 | interface IERC165 { 19 | function supportsInterface(bytes4 interfaceId) external view returns (bool); 20 | } 21 | 22 | interface IERC721 is IERC165 { 23 | event Transfer( 24 | address indexed from, 25 | address indexed to, 26 | uint256 indexed tokenId 27 | ); 28 | event Approval( 29 | address indexed owner, 30 | address indexed approved, 31 | uint256 indexed tokenId 32 | ); 33 | event ApprovalForAll( 34 | address indexed owner, 35 | address indexed operator, 36 | bool approved 37 | ); 38 | 39 | function balanceOf(address owner) external view returns (uint256 balance); 40 | 41 | function ownerOf(uint256 tokenId) external view returns (address owner); 42 | 43 | function safeTransferFrom( 44 | address from, 45 | address to, 46 | uint256 tokenId 47 | ) external; 48 | 49 | function transferFrom( 50 | address from, 51 | address to, 52 | uint256 tokenId 53 | ) external; 54 | 55 | function approve(address to, uint256 tokenId) external; 56 | 57 | function getApproved(uint256 tokenId) 58 | external 59 | view 60 | returns (address operator); 61 | 62 | function setApprovalForAll(address operator, bool _approved) external; 63 | 64 | function isApprovedForAll(address owner, address operator) 65 | external 66 | view 67 | returns (bool); 68 | 69 | function safeTransferFrom( 70 | address from, 71 | address to, 72 | uint256 tokenId, 73 | bytes calldata data 74 | ) external; 75 | } 76 | 77 | /** 78 | * Minimal interface to Cryptopunks for verifying ownership during Community Grant. 79 | */ 80 | interface Cryptopunks { 81 | function punkIndexToAddress(uint256 index) external view returns (address); 82 | } 83 | 84 | interface ERC721TokenReceiver { 85 | function onERC721Received( 86 | address _operator, 87 | address _from, 88 | uint256 _tokenId, 89 | bytes calldata _data 90 | ) external returns (bytes4); 91 | } 92 | 93 | library SafeMath { 94 | /** 95 | * @dev Multiplies two numbers, throws on overflow. 96 | */ 97 | function mul(uint256 a, uint256 b) internal pure returns (uint256 c) { 98 | if (a == 0) { 99 | return 0; 100 | } 101 | c = a * b; 102 | require(c / a == b); 103 | return c; 104 | } 105 | 106 | /** 107 | * @dev Integer division of two numbers, truncating the quotient. 108 | */ 109 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 110 | // assert(b > 0); // Solidity automatically throws when dividing by 0 111 | // uint256 c = a / b; 112 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 113 | return a / b; 114 | } 115 | 116 | /** 117 | * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). 118 | */ 119 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 120 | require(b <= a); 121 | return a - b; 122 | } 123 | 124 | /** 125 | * @dev Adds two numbers, throws on overflow. 126 | */ 127 | function add(uint256 a, uint256 b) internal pure returns (uint256 c) { 128 | c = a + b; 129 | require(c >= a); 130 | return c; 131 | } 132 | } 133 | 134 | contract Meebits is IERC721 { 135 | using SafeMath for uint256; 136 | 137 | /** 138 | * Event emitted when minting a new NFT. "createdVia" is the index of the Cryptopunk/Autoglyph that was used to mint, or 0 if not applicable. 139 | */ 140 | event Mint( 141 | uint256 indexed index, 142 | address indexed minter, 143 | uint256 createdVia 144 | ); 145 | 146 | /** 147 | * Event emitted when a trade is executed. 148 | */ 149 | event Trade( 150 | bytes32 indexed hash, 151 | address indexed maker, 152 | address taker, 153 | uint256 makerWei, 154 | uint256[] makerIds, 155 | uint256 takerWei, 156 | uint256[] takerIds 157 | ); 158 | 159 | /** 160 | * Event emitted when ETH is deposited into the contract. 161 | */ 162 | event Deposit(address indexed account, uint256 amount); 163 | 164 | /** 165 | * Event emitted when ETH is withdrawn from the contract. 166 | */ 167 | event Withdraw(address indexed account, uint256 amount); 168 | 169 | /** 170 | * Event emitted when a trade offer is cancelled. 171 | */ 172 | event OfferCancelled(bytes32 hash); 173 | 174 | /** 175 | * Event emitted when the public sale begins. 176 | */ 177 | event SaleBegins(); 178 | 179 | /** 180 | * Event emitted when the community grant period ends. 181 | */ 182 | event CommunityGrantEnds(); 183 | 184 | bytes4 internal constant MAGIC_ON_ERC721_RECEIVED = 0x150b7a02; 185 | 186 | // IPFS Hash to the NFT content 187 | string public contentHash = 188 | "QmfXYgfX1qNfzQ6NRyFnupniZusasFPMeiWn5aaDnx7YXo"; 189 | 190 | uint256 public constant TOKEN_LIMIT = 20000; 191 | uint256 public constant SALE_LIMIT = 9000; 192 | 193 | mapping(bytes4 => bool) internal supportedInterfaces; 194 | 195 | mapping(uint256 => address) internal idToOwner; 196 | 197 | mapping(uint256 => uint256) public creatorNftMints; 198 | 199 | mapping(uint256 => address) internal idToApproval; 200 | 201 | mapping(address => mapping(address => bool)) internal ownerToOperators; 202 | 203 | mapping(address => uint256[]) internal ownerToIds; 204 | 205 | mapping(uint256 => uint256) internal idToOwnerIndex; 206 | 207 | string internal nftName = "Meebits"; 208 | string internal nftSymbol = unicode"⚇"; 209 | 210 | uint256 internal numTokens = 0; 211 | uint256 internal numSales = 0; 212 | 213 | // Cryptopunks contract 214 | address internal punks; 215 | 216 | // Autoglyphs contract 217 | address internal glyphs; 218 | 219 | address payable internal deployer; 220 | address payable internal beneficiary; 221 | bool public communityGrant = true; 222 | bool public publicSale = false; 223 | uint256 private price; 224 | uint256 public saleStartTime; 225 | uint256 public saleDuration; 226 | 227 | //// Random index assignment 228 | uint256 internal nonce = 0; 229 | uint256[TOKEN_LIMIT] internal indices; 230 | 231 | //// Market 232 | bool public marketPaused; 233 | bool public contractSealed; 234 | mapping(address => uint256) public ethBalance; 235 | mapping(bytes32 => bool) public cancelledOffers; 236 | 237 | modifier onlyDeployer() { 238 | require(msg.sender == deployer, "Only deployer."); 239 | _; 240 | } 241 | 242 | bool private reentrancyLock = false; 243 | 244 | /* Prevent a contract function from being reentrant-called. */ 245 | modifier reentrancyGuard() { 246 | if (reentrancyLock) { 247 | revert(); 248 | } 249 | reentrancyLock = true; 250 | _; 251 | reentrancyLock = false; 252 | } 253 | 254 | modifier canOperate(uint256 _tokenId) { 255 | address tokenOwner = idToOwner[_tokenId]; 256 | require( 257 | tokenOwner == msg.sender || 258 | ownerToOperators[tokenOwner][msg.sender], 259 | "Cannot operate." 260 | ); 261 | _; 262 | } 263 | 264 | modifier canTransfer(uint256 _tokenId) { 265 | address tokenOwner = idToOwner[_tokenId]; 266 | require( 267 | tokenOwner == msg.sender || 268 | idToApproval[_tokenId] == msg.sender || 269 | ownerToOperators[tokenOwner][msg.sender], 270 | "Cannot transfer." 271 | ); 272 | _; 273 | } 274 | 275 | modifier validNFToken(uint256 _tokenId) { 276 | require(idToOwner[_tokenId] != address(0), "Invalid token."); 277 | _; 278 | } 279 | 280 | constructor( 281 | address _punks, 282 | address _glyphs, 283 | address payable _beneficiary 284 | ) { 285 | supportedInterfaces[0x01ffc9a7] = true; // ERC165 286 | supportedInterfaces[0x80ac58cd] = true; // ERC721 287 | supportedInterfaces[0x780e9d63] = true; // ERC721 Enumerable 288 | supportedInterfaces[0x5b5e139f] = true; // ERC721 Metadata 289 | deployer = msg.sender; 290 | punks = _punks; 291 | glyphs = _glyphs; 292 | beneficiary = _beneficiary; 293 | } 294 | 295 | function startSale(uint256 _price, uint256 _saleDuration) 296 | external 297 | onlyDeployer 298 | { 299 | require(!publicSale); 300 | price = _price; 301 | saleDuration = _saleDuration; 302 | saleStartTime = block.timestamp; 303 | publicSale = true; 304 | emit SaleBegins(); 305 | } 306 | 307 | function endCommunityGrant() external onlyDeployer { 308 | require(communityGrant); 309 | communityGrant = false; 310 | emit CommunityGrantEnds(); 311 | } 312 | 313 | function pauseMarket(bool _paused) external onlyDeployer { 314 | require(!contractSealed, "Contract sealed."); 315 | marketPaused = _paused; 316 | } 317 | 318 | function sealContract() external onlyDeployer { 319 | contractSealed = true; 320 | } 321 | 322 | ////////////////////////// 323 | //// ERC 721 and 165 //// 324 | ////////////////////////// 325 | 326 | function isContract(address _addr) 327 | internal 328 | view 329 | returns (bool addressCheck) 330 | { 331 | uint256 size; 332 | assembly { 333 | size := extcodesize(_addr) 334 | } // solhint-disable-line 335 | addressCheck = size > 0; 336 | } 337 | 338 | function supportsInterface(bytes4 _interfaceID) 339 | external 340 | view 341 | override 342 | returns (bool) 343 | { 344 | return supportedInterfaces[_interfaceID]; 345 | } 346 | 347 | function safeTransferFrom( 348 | address _from, 349 | address _to, 350 | uint256 _tokenId, 351 | bytes calldata _data 352 | ) external override { 353 | _safeTransferFrom(_from, _to, _tokenId, _data); 354 | } 355 | 356 | function safeTransferFrom( 357 | address _from, 358 | address _to, 359 | uint256 _tokenId 360 | ) external override { 361 | _safeTransferFrom(_from, _to, _tokenId, ""); 362 | } 363 | 364 | function transferFrom( 365 | address _from, 366 | address _to, 367 | uint256 _tokenId 368 | ) external override canTransfer(_tokenId) validNFToken(_tokenId) { 369 | address tokenOwner = idToOwner[_tokenId]; 370 | require(tokenOwner == _from, "Wrong from address."); 371 | require(_to != address(0), "Cannot send to 0x0."); 372 | _transfer(_to, _tokenId); 373 | } 374 | 375 | function approve(address _approved, uint256 _tokenId) 376 | external 377 | override 378 | canOperate(_tokenId) 379 | validNFToken(_tokenId) 380 | { 381 | address tokenOwner = idToOwner[_tokenId]; 382 | require(_approved != tokenOwner); 383 | idToApproval[_tokenId] = _approved; 384 | emit Approval(tokenOwner, _approved, _tokenId); 385 | } 386 | 387 | function setApprovalForAll(address _operator, bool _approved) 388 | external 389 | override 390 | { 391 | ownerToOperators[msg.sender][_operator] = _approved; 392 | emit ApprovalForAll(msg.sender, _operator, _approved); 393 | } 394 | 395 | function balanceOf(address _owner) 396 | external 397 | view 398 | override 399 | returns (uint256) 400 | { 401 | require(_owner != address(0)); 402 | return _getOwnerNFTCount(_owner); 403 | } 404 | 405 | function ownerOf(uint256 _tokenId) 406 | external 407 | view 408 | override 409 | returns (address _owner) 410 | { 411 | require(idToOwner[_tokenId] != address(0)); 412 | _owner = idToOwner[_tokenId]; 413 | } 414 | 415 | function getApproved(uint256 _tokenId) 416 | external 417 | view 418 | override 419 | validNFToken(_tokenId) 420 | returns (address) 421 | { 422 | return idToApproval[_tokenId]; 423 | } 424 | 425 | function isApprovedForAll(address _owner, address _operator) 426 | external 427 | view 428 | override 429 | returns (bool) 430 | { 431 | return ownerToOperators[_owner][_operator]; 432 | } 433 | 434 | function _transfer(address _to, uint256 _tokenId) internal { 435 | address from = idToOwner[_tokenId]; 436 | _clearApproval(_tokenId); 437 | 438 | _removeNFToken(from, _tokenId); 439 | _addNFToken(_to, _tokenId); 440 | 441 | emit Transfer(from, _to, _tokenId); 442 | } 443 | 444 | function randomIndex() internal returns (uint256) { 445 | uint256 totalSize = TOKEN_LIMIT - numTokens; 446 | uint256 index = uint256( 447 | keccak256( 448 | abi.encodePacked( 449 | nonce, 450 | msg.sender, 451 | block.difficulty, 452 | block.timestamp 453 | ) 454 | ) 455 | ) % totalSize; 456 | uint256 value = 0; 457 | if (indices[index] != 0) { 458 | value = indices[index]; 459 | } else { 460 | value = index; 461 | } 462 | 463 | // Move last value to selected position 464 | if (indices[totalSize - 1] == 0) { 465 | // Array position not initialized, so use position 466 | indices[index] = totalSize - 1; 467 | } else { 468 | // Array position holds a value so use that 469 | indices[index] = indices[totalSize - 1]; 470 | } 471 | nonce++; 472 | // Don't allow a zero index, start counting at 1 473 | return value.add(1); 474 | } 475 | 476 | // Calculate the mint price 477 | function getPrice() public view returns (uint256) { 478 | require(publicSale, "Sale not started."); 479 | uint256 elapsed = block.timestamp.sub(saleStartTime); 480 | if (elapsed >= saleDuration) { 481 | return 0; 482 | } else { 483 | return saleDuration.sub(elapsed).mul(price).div(saleDuration); 484 | } 485 | } 486 | 487 | // The deployer can mint in bulk without paying 488 | function devMint(uint256 quantity, address recipient) 489 | external 490 | onlyDeployer 491 | { 492 | for (uint256 i = 0; i < quantity; i++) { 493 | _mint(recipient, 0); 494 | } 495 | } 496 | 497 | function mintsRemaining() external view returns (uint256) { 498 | return SALE_LIMIT.sub(numSales); 499 | } 500 | 501 | /** 502 | * Community grant minting. 503 | */ 504 | function mintWithPunkOrGlyph(uint256 _createVia) 505 | external 506 | reentrancyGuard 507 | returns (uint256) 508 | { 509 | require(communityGrant); 510 | require(!marketPaused); 511 | require( 512 | _createVia > 0 && _createVia <= 10512, 513 | "Invalid punk/glyph index." 514 | ); 515 | require( 516 | creatorNftMints[_createVia] == 0, 517 | "Already minted with this punk/glyph" 518 | ); 519 | if (_createVia > 10000) { 520 | // It's a glyph 521 | // Compute the glyph ID 522 | uint256 glyphId = _createVia.sub(10000); 523 | // Make sure the sender owns the glyph 524 | require( 525 | IERC721(glyphs).ownerOf(glyphId) == msg.sender, 526 | "Not the owner of this glyph." 527 | ); 528 | } else { 529 | // It's a punk 530 | // Compute the punk ID 531 | uint256 punkId = _createVia.sub(1); 532 | // Make sure the sender owns the punk 533 | require( 534 | Cryptopunks(punks).punkIndexToAddress(punkId) == msg.sender, 535 | "Not the owner of this punk." 536 | ); 537 | } 538 | creatorNftMints[_createVia]++; 539 | return _mint(msg.sender, _createVia); 540 | } 541 | 542 | /** 543 | * Public sale minting. 544 | */ 545 | function mint() external payable reentrancyGuard returns (uint256) { 546 | require(publicSale, "Sale not started."); 547 | require(!marketPaused); 548 | require(numSales < SALE_LIMIT, "Sale limit reached."); 549 | uint256 salePrice = getPrice(); 550 | require(msg.value >= salePrice, "Insufficient funds to purchase."); 551 | if (msg.value > salePrice) { 552 | msg.sender.transfer(msg.value.sub(salePrice)); 553 | } 554 | beneficiary.transfer(salePrice); 555 | numSales++; 556 | return _mint(msg.sender, 0); 557 | } 558 | 559 | function _mint(address _to, uint256 createdVia) internal returns (uint256) { 560 | require(_to != address(0), "Cannot mint to 0x0."); 561 | require(numTokens < TOKEN_LIMIT, "Token limit reached."); 562 | uint256 id = randomIndex(); 563 | 564 | numTokens = numTokens + 1; 565 | _addNFToken(_to, id); 566 | 567 | emit Mint(id, _to, createdVia); 568 | emit Transfer(address(0), _to, id); 569 | return id; 570 | } 571 | 572 | function _addNFToken(address _to, uint256 _tokenId) internal { 573 | require( 574 | idToOwner[_tokenId] == address(0), 575 | "Cannot add, already owned." 576 | ); 577 | idToOwner[_tokenId] = _to; 578 | 579 | ownerToIds[_to].push(_tokenId); 580 | idToOwnerIndex[_tokenId] = ownerToIds[_to].length.sub(1); 581 | } 582 | 583 | function _removeNFToken(address _from, uint256 _tokenId) internal { 584 | require(idToOwner[_tokenId] == _from, "Incorrect owner."); 585 | delete idToOwner[_tokenId]; 586 | 587 | uint256 tokenToRemoveIndex = idToOwnerIndex[_tokenId]; 588 | uint256 lastTokenIndex = ownerToIds[_from].length.sub(1); 589 | 590 | if (lastTokenIndex != tokenToRemoveIndex) { 591 | uint256 lastToken = ownerToIds[_from][lastTokenIndex]; 592 | ownerToIds[_from][tokenToRemoveIndex] = lastToken; 593 | idToOwnerIndex[lastToken] = tokenToRemoveIndex; 594 | } 595 | 596 | ownerToIds[_from].pop(); 597 | } 598 | 599 | function _getOwnerNFTCount(address _owner) internal view returns (uint256) { 600 | return ownerToIds[_owner].length; 601 | } 602 | 603 | function _safeTransferFrom( 604 | address _from, 605 | address _to, 606 | uint256 _tokenId, 607 | bytes memory _data 608 | ) private canTransfer(_tokenId) validNFToken(_tokenId) { 609 | address tokenOwner = idToOwner[_tokenId]; 610 | require(tokenOwner == _from, "Incorrect owner."); 611 | require(_to != address(0)); 612 | 613 | _transfer(_to, _tokenId); 614 | 615 | if (isContract(_to)) { 616 | bytes4 retval = ERC721TokenReceiver(_to).onERC721Received( 617 | msg.sender, 618 | _from, 619 | _tokenId, 620 | _data 621 | ); 622 | require(retval == MAGIC_ON_ERC721_RECEIVED); 623 | } 624 | } 625 | 626 | function _clearApproval(uint256 _tokenId) private { 627 | if (idToApproval[_tokenId] != address(0)) { 628 | delete idToApproval[_tokenId]; 629 | } 630 | } 631 | 632 | //// Enumerable 633 | 634 | function totalSupply() public view returns (uint256) { 635 | return numTokens; 636 | } 637 | 638 | function tokenByIndex(uint256 index) public pure returns (uint256) { 639 | require(index >= 0 && index < TOKEN_LIMIT); 640 | return index + 1; 641 | } 642 | 643 | function tokenOfOwnerByIndex(address _owner, uint256 _index) 644 | external 645 | view 646 | returns (uint256) 647 | { 648 | require(_index < ownerToIds[_owner].length); 649 | return ownerToIds[_owner][_index]; 650 | } 651 | 652 | //// Metadata 653 | 654 | /** 655 | * @dev Converts a `uint256` to its ASCII `string` representation. 656 | */ 657 | function toString(uint256 value) internal pure returns (string memory) { 658 | if (value == 0) { 659 | return "0"; 660 | } 661 | uint256 temp = value; 662 | uint256 digits; 663 | while (temp != 0) { 664 | digits++; 665 | temp /= 10; 666 | } 667 | bytes memory buffer = new bytes(digits); 668 | uint256 index = digits - 1; 669 | temp = value; 670 | while (temp != 0) { 671 | buffer[index--] = bytes1(uint8(48 + (temp % 10))); 672 | temp /= 10; 673 | } 674 | return string(buffer); 675 | } 676 | 677 | /** 678 | * @dev Returns a descriptive name for a collection of NFTokens. 679 | * @return _name Representing name. 680 | */ 681 | function name() external view returns (string memory _name) { 682 | _name = nftName; 683 | } 684 | 685 | /** 686 | * @dev Returns an abbreviated name for NFTokens. 687 | * @return _symbol Representing symbol. 688 | */ 689 | function symbol() external view returns (string memory _symbol) { 690 | _symbol = nftSymbol; 691 | } 692 | 693 | /** 694 | * @dev A distinct URI (RFC 3986) for a given NFT. 695 | * @param _tokenId Id for which we want uri. 696 | * @return _tokenId URI of _tokenId. 697 | */ 698 | function tokenURI(uint256 _tokenId) 699 | external 700 | view 701 | validNFToken(_tokenId) 702 | returns (string memory) 703 | { 704 | return 705 | string( 706 | abi.encodePacked( 707 | "https://meebits.larvalabs.com/meebit/", 708 | toString(_tokenId) 709 | ) 710 | ); 711 | } 712 | 713 | //// MARKET 714 | 715 | struct Offer { 716 | address maker; 717 | address taker; 718 | uint256 makerWei; 719 | uint256[] makerIds; 720 | uint256 takerWei; 721 | uint256[] takerIds; 722 | uint256 expiry; 723 | uint256 salt; 724 | } 725 | 726 | function hashOffer(Offer memory offer) private pure returns (bytes32) { 727 | return 728 | keccak256( 729 | abi.encode( 730 | offer.maker, 731 | offer.taker, 732 | offer.makerWei, 733 | keccak256(abi.encodePacked(offer.makerIds)), 734 | offer.takerWei, 735 | keccak256(abi.encodePacked(offer.takerIds)), 736 | offer.expiry, 737 | offer.salt 738 | ) 739 | ); 740 | } 741 | 742 | function hashToSign( 743 | address maker, 744 | address taker, 745 | uint256 makerWei, 746 | uint256[] memory makerIds, 747 | uint256 takerWei, 748 | uint256[] memory takerIds, 749 | uint256 expiry, 750 | uint256 salt 751 | ) public pure returns (bytes32) { 752 | Offer memory offer = Offer( 753 | maker, 754 | taker, 755 | makerWei, 756 | makerIds, 757 | takerWei, 758 | takerIds, 759 | expiry, 760 | salt 761 | ); 762 | return hashOffer(offer); 763 | } 764 | 765 | function hashToVerify(Offer memory offer) private pure returns (bytes32) { 766 | return 767 | keccak256( 768 | abi.encodePacked( 769 | "\x19Ethereum Signed Message:\n32", 770 | hashOffer(offer) 771 | ) 772 | ); 773 | } 774 | 775 | function verify( 776 | address signer, 777 | bytes32 hash, 778 | bytes memory signature 779 | ) internal pure returns (bool) { 780 | require(signer != address(0)); 781 | require(signature.length == 65); 782 | 783 | bytes32 r; 784 | bytes32 s; 785 | uint8 v; 786 | 787 | assembly { 788 | r := mload(add(signature, 32)) 789 | s := mload(add(signature, 64)) 790 | v := byte(0, mload(add(signature, 96))) 791 | } 792 | 793 | if (v < 27) { 794 | v += 27; 795 | } 796 | 797 | require(v == 27 || v == 28); 798 | 799 | return signer == ecrecover(hash, v, r, s); 800 | } 801 | 802 | function tradeValid( 803 | address maker, 804 | address taker, 805 | uint256 makerWei, 806 | uint256[] memory makerIds, 807 | uint256 takerWei, 808 | uint256[] memory takerIds, 809 | uint256 expiry, 810 | uint256 salt, 811 | bytes memory signature 812 | ) public view returns (bool) { 813 | Offer memory offer = Offer( 814 | maker, 815 | taker, 816 | makerWei, 817 | makerIds, 818 | takerWei, 819 | takerIds, 820 | expiry, 821 | salt 822 | ); 823 | // Check for cancellation 824 | bytes32 hash = hashOffer(offer); 825 | require(cancelledOffers[hash] == false, "Trade offer was cancelled."); 826 | // Verify signature 827 | bytes32 verifyHash = hashToVerify(offer); 828 | require( 829 | verify(offer.maker, verifyHash, signature), 830 | "Signature not valid." 831 | ); 832 | // Check for expiry 833 | require(block.timestamp < offer.expiry, "Trade offer expired."); 834 | // Only one side should ever have to pay, not both 835 | require( 836 | makerWei == 0 || takerWei == 0, 837 | "Only one side of trade must pay." 838 | ); 839 | // At least one side should offer tokens 840 | require( 841 | makerIds.length > 0 || takerIds.length > 0, 842 | "One side must offer tokens." 843 | ); 844 | // Make sure the maker has funded the trade 845 | require( 846 | ethBalance[offer.maker] >= offer.makerWei, 847 | "Maker does not have sufficient balance." 848 | ); 849 | // Ensure the maker owns the maker tokens 850 | for (uint256 i = 0; i < offer.makerIds.length; i++) { 851 | require( 852 | idToOwner[offer.makerIds[i]] == offer.maker, 853 | "At least one maker token doesn't belong to maker." 854 | ); 855 | } 856 | // If the taker can be anybody, then there can be no taker tokens 857 | if (offer.taker == address(0)) { 858 | // If taker not specified, then can't specify IDs 859 | require( 860 | offer.takerIds.length == 0, 861 | "If trade is offered to anybody, cannot specify tokens from taker." 862 | ); 863 | } else { 864 | // Ensure the taker owns the taker tokens 865 | for (uint256 i = 0; i < offer.takerIds.length; i++) { 866 | require( 867 | idToOwner[offer.takerIds[i]] == offer.taker, 868 | "At least one taker token doesn't belong to taker." 869 | ); 870 | } 871 | } 872 | return true; 873 | } 874 | 875 | function cancelOffer( 876 | address maker, 877 | address taker, 878 | uint256 makerWei, 879 | uint256[] memory makerIds, 880 | uint256 takerWei, 881 | uint256[] memory takerIds, 882 | uint256 expiry, 883 | uint256 salt 884 | ) external { 885 | require(maker == msg.sender, "Only the maker can cancel this offer."); 886 | Offer memory offer = Offer( 887 | maker, 888 | taker, 889 | makerWei, 890 | makerIds, 891 | takerWei, 892 | takerIds, 893 | expiry, 894 | salt 895 | ); 896 | bytes32 hash = hashOffer(offer); 897 | cancelledOffers[hash] = true; 898 | emit OfferCancelled(hash); 899 | } 900 | 901 | function acceptTrade( 902 | address maker, 903 | address taker, 904 | uint256 makerWei, 905 | uint256[] memory makerIds, 906 | uint256 takerWei, 907 | uint256[] memory takerIds, 908 | uint256 expiry, 909 | uint256 salt, 910 | bytes memory signature 911 | ) external payable reentrancyGuard { 912 | require(!marketPaused, "Market is paused."); 913 | require(msg.sender != maker, "Can't accept ones own trade."); 914 | Offer memory offer = Offer( 915 | maker, 916 | taker, 917 | makerWei, 918 | makerIds, 919 | takerWei, 920 | takerIds, 921 | expiry, 922 | salt 923 | ); 924 | if (msg.value > 0) { 925 | ethBalance[msg.sender] = ethBalance[msg.sender].add(msg.value); 926 | emit Deposit(msg.sender, msg.value); 927 | } 928 | require( 929 | offer.taker == address(0) || offer.taker == msg.sender, 930 | "Not the recipient of this offer." 931 | ); 932 | require( 933 | tradeValid( 934 | maker, 935 | taker, 936 | makerWei, 937 | makerIds, 938 | takerWei, 939 | takerIds, 940 | expiry, 941 | salt, 942 | signature 943 | ), 944 | "Trade not valid." 945 | ); 946 | require( 947 | ethBalance[msg.sender] >= offer.takerWei, 948 | "Insufficient funds to execute trade." 949 | ); 950 | // Transfer ETH 951 | ethBalance[offer.maker] = ethBalance[offer.maker].sub(offer.makerWei); 952 | ethBalance[msg.sender] = ethBalance[msg.sender].add(offer.makerWei); 953 | ethBalance[msg.sender] = ethBalance[msg.sender].sub(offer.takerWei); 954 | ethBalance[offer.maker] = ethBalance[offer.maker].add(offer.takerWei); 955 | // Transfer maker ids to taker (msg.sender) 956 | for (uint256 i = 0; i < makerIds.length; i++) { 957 | _transfer(msg.sender, makerIds[i]); 958 | } 959 | // Transfer taker ids to maker 960 | for (uint256 i = 0; i < takerIds.length; i++) { 961 | _transfer(maker, takerIds[i]); 962 | } 963 | // Prevent a replay attack on this offer 964 | bytes32 hash = hashOffer(offer); 965 | cancelledOffers[hash] = true; 966 | emit Trade( 967 | hash, 968 | offer.maker, 969 | msg.sender, 970 | offer.makerWei, 971 | offer.makerIds, 972 | offer.takerWei, 973 | offer.takerIds 974 | ); 975 | } 976 | 977 | function withdraw(uint256 amount) external reentrancyGuard { 978 | require(amount <= ethBalance[msg.sender]); 979 | ethBalance[msg.sender] = ethBalance[msg.sender].sub(amount); 980 | (bool success, ) = msg.sender.call{value: amount}(""); 981 | require(success); 982 | emit Withdraw(msg.sender, amount); 983 | } 984 | 985 | function deposit() external payable { 986 | ethBalance[msg.sender] = ethBalance[msg.sender].add(msg.value); 987 | emit Deposit(msg.sender, msg.value); 988 | } 989 | } 990 | -------------------------------------------------------------------------------- /Meebits/img/fail_brute_force_mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/Meebits/img/fail_brute_force_mint.png -------------------------------------------------------------------------------- /Meebits/img/successful_brute_force_mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/Meebits/img/successful_brute_force_mint.png -------------------------------------------------------------------------------- /MultiSig/Analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis of the Multi-Sig exploit 2 | 3 | Attacker address: [0xB3764761E297D6f121e79C32A65829Cd1dDb4D32](https://etherscan.io/address/0xb3764761e297d6f121e79c32a65829cd1ddb4d32?fromaddress=0xB3764761E297D6f121e79C32A65829Cd1dDb4D32) 4 | Victim contract #1: [0x91EFffB9C6cd3A66474688D0a48AA6ECfe515AA5](https://etherscan.io/address/0x91efffb9c6cd3a66474688d0a48aa6ecfe515aa5#code) 5 | Wallet Library: [0x4f2875f631f4fc66b8e051defba0c9f9106d7d5a](https://etherscan.io/address/0x4f2875f631f4fc66b8e051defba0c9f9106d7d5a#code) 6 | 7 | ## The vulnerability: Delegate Call with multiple Initializatons possible 8 | 9 | The multi sig wallet works with a library and use `delegatecall`. If someone calls `initWallet()` on the Wallet, it will delegate the call to the Library which will execute **within the context** of the Wallet contract: 10 | ```solidity 11 | function initWallet( 12 | address[] _owners, 13 | uint256 _required, 14 | uint256 _daylimit 15 | ) { 16 | initMultiowned(_owners, _required); //sett owner and n. of required sig 17 | initDaylimit(_daylimit); // set daily limit for withdraw 18 | } 19 | ``` 20 | With `initMultiowned()`: 21 | ```solidity 22 | // constructor is given number of sigs required to do protected "onlymanyowners" transactions 23 | // as well as the selection of addresses capable of confirming them. 24 | // change from original: msg.sender is not automatically owner 25 | function initMultiowned(address[] _owners, uint256 _required) { 26 | m_numOwners = _owners.length; //1 27 | m_required = _required; //0 28 | 29 | for (uint256 i = 0; i < _owners.length; ++i) { 30 | m_owners[1 + i] = uint256(_owners[i]); //attacker becomes owner 31 | m_ownerIndex[uint256(_owners[i])] = 1 + i; 32 | } 33 | } 34 | ``` 35 | So by calling `initWallet([attacker_addr], 0,_)`, the attacker becomes owner of the Wallet, change the number of signature required to 0 and the daily limit of widrawnable funds to the balance of the wallet. He can then withdraw the funds by calling `execute(attacker_addr, amount, _)`: 36 | ```solidity 37 | // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. 38 | // If not, goes into multisig process. We provide a hash on return to allow the sender to provide 39 | // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value 40 | // and _data arguments). They still get the option of using them if they want, anyways. 41 | function execute( 42 | address _to, 43 | uint256 _value, 44 | bytes _data 45 | ) onlyowner returns (bool _callValue) { 46 | // first, take the opportunity to check that we're under the daily limit. 47 | if (underLimit(_value)) { 48 | SingleTransact(msg.sender, _value, _to, _data); 49 | // yes - just execute the call. 50 | _callValue = _to.call.value(_value)(_data); // transfer funds to attacker 51 | } else {...} 52 | } 53 | ``` 54 | 55 | 56 | ## The Exploit 57 | The attacker send a first transaction `initWallet` to become owner, and then a second `execute` to withdraw the funds: 58 | 59 | ![Transactions](img/attacker_txs.png "Transactions") 60 | After that he received the ETH on his account. 61 | 62 | The first transaction calls `initWallet([attacker_addr], 0, amount_to_withdraw)`, since Wallet doesn't have this function signature, it delegate the call to the WalletLibrary which end up modifying the variable in Wallet 63 | ![Init transaction](img/init_tx.png "Init transaction") 64 | 65 | The second transaction calls `execute(attacker_addr, balanceOf(contract)` which once again will get delegated to the library which will transfer the amount given as argument to the address given as parameter. 66 | ![Execute transaction](img/execute_tx.png "Execute transaction") 67 | 68 | ## The Aftermath 69 | The same attack was executed on three similar wallet, for a total of over 150,000 ETH. There is still about 83,000 ETH in the hacker account ($107,371,137 at this time) while arount 70,000 ETH have been transfer to other EOA. -------------------------------------------------------------------------------- /MultiSig/Wallet.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2017-02-22 3 | */ 4 | 5 | // This multisignature wallet is based on the wallet contract by Gav Wood. 6 | // Only one single change was made: The contract creator is not automatically one of the wallet owners. 7 | 8 | //sol Wallet 9 | // Multi-sig, daily-limited account proxy/wallet. 10 | // @authors: 11 | // Gav Wood 12 | // inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a 13 | // single, or, crucially, each of a number of, designated owners. 14 | // usage: 15 | // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by 16 | // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the 17 | // interior is executed. 18 | pragma solidity ^0.4.6; 19 | 20 | contract multisig { 21 | // EVENTS 22 | 23 | // this contract can accept a confirmation, in which case 24 | // we record owner and operation (hash) alongside it. 25 | event Confirmation(address owner, bytes32 operation); 26 | event Revoke(address owner, bytes32 operation); 27 | 28 | // some others are in the case of an owner changing. 29 | event OwnerChanged(address oldOwner, address newOwner); 30 | event OwnerAdded(address newOwner); 31 | event OwnerRemoved(address oldOwner); 32 | 33 | // the last one is emitted if the required signatures change 34 | event RequirementChanged(uint256 newRequirement); 35 | 36 | // Funds has arrived into the wallet (record how much). 37 | event Deposit(address _from, uint256 value); 38 | // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). 39 | event SingleTransact(address owner, uint256 value, address to, bytes data); 40 | // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). 41 | event MultiTransact( 42 | address owner, 43 | bytes32 operation, 44 | uint256 value, 45 | address to, 46 | bytes data 47 | ); 48 | // Confirmation still needed for a transaction. 49 | event ConfirmationNeeded( 50 | bytes32 operation, 51 | address initiator, 52 | uint256 value, 53 | address to, 54 | bytes data 55 | ); 56 | } 57 | 58 | contract multisigAbi is multisig { 59 | function isOwner(address _addr) returns (bool); 60 | 61 | function hasConfirmed(bytes32 _operation, address _owner) 62 | constant 63 | returns (bool); 64 | 65 | function confirm(bytes32 _h) returns (bool); 66 | 67 | // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. 68 | function setDailyLimit(uint256 _newLimit); 69 | 70 | function addOwner(address _owner); 71 | 72 | function removeOwner(address _owner); 73 | 74 | function changeRequirement(uint256 _newRequired); 75 | 76 | // Revokes a prior confirmation of the given operation 77 | function revoke(bytes32 _operation); 78 | 79 | function changeOwner(address _from, address _to); 80 | 81 | function execute( 82 | address _to, 83 | uint256 _value, 84 | bytes _data 85 | ) returns (bool); 86 | } 87 | 88 | contract WalletLibrary is multisig { 89 | // TYPES 90 | 91 | // struct for the status of a pending operation. 92 | struct PendingState { 93 | uint256 yetNeeded; 94 | uint256 ownersDone; 95 | uint256 index; 96 | } 97 | 98 | // Transaction structure to remember details of transaction lest it need be saved for a later call. 99 | struct Transaction { 100 | address to; 101 | uint256 value; 102 | bytes data; 103 | } 104 | 105 | /****************************** 106 | ***** MULTI OWNED SECTION **** 107 | ******************************/ 108 | 109 | // MODIFIERS 110 | 111 | // simple single-sig function modifier. 112 | modifier onlyowner() { 113 | if (isOwner(msg.sender)) _; 114 | } 115 | // multi-sig function modifier: the operation must have an intrinsic hash in order 116 | // that later attempts can be realised as the same underlying operation and 117 | // thus count as confirmations. 118 | modifier onlymanyowners(bytes32 _operation) { 119 | if (confirmAndCheck(_operation)) _; 120 | } 121 | 122 | // METHODS 123 | 124 | // constructor is given number of sigs required to do protected "onlymanyowners" transactions 125 | // as well as the selection of addresses capable of confirming them. 126 | // change from original: msg.sender is not automatically owner 127 | function initMultiowned(address[] _owners, uint256 _required) { 128 | m_numOwners = _owners.length; 129 | m_required = _required; 130 | 131 | for (uint256 i = 0; i < _owners.length; ++i) { 132 | m_owners[1 + i] = uint256(_owners[i]); 133 | m_ownerIndex[uint256(_owners[i])] = 1 + i; 134 | } 135 | } 136 | 137 | // Revokes a prior confirmation of the given operation 138 | function revoke(bytes32 _operation) { 139 | uint256 ownerIndex = m_ownerIndex[uint256(msg.sender)]; 140 | // make sure they're an owner 141 | if (ownerIndex == 0) return; 142 | uint256 ownerIndexBit = 2**ownerIndex; 143 | var pending = m_pending[_operation]; 144 | if (pending.ownersDone & ownerIndexBit > 0) { 145 | pending.yetNeeded++; 146 | pending.ownersDone -= ownerIndexBit; 147 | Revoke(msg.sender, _operation); 148 | } 149 | } 150 | 151 | // Replaces an owner `_from` with another `_to`. 152 | function changeOwner(address _from, address _to) 153 | onlymanyowners(sha3(msg.data)) 154 | { 155 | if (isOwner(_to)) return; 156 | uint256 ownerIndex = m_ownerIndex[uint256(_from)]; 157 | if (ownerIndex == 0) return; 158 | 159 | clearPending(); 160 | m_owners[ownerIndex] = uint256(_to); 161 | m_ownerIndex[uint256(_from)] = 0; 162 | m_ownerIndex[uint256(_to)] = ownerIndex; 163 | OwnerChanged(_from, _to); 164 | } 165 | 166 | function addOwner(address _owner) onlymanyowners(sha3(msg.data)) { 167 | if (isOwner(_owner)) return; 168 | 169 | clearPending(); 170 | if (m_numOwners >= c_maxOwners) reorganizeOwners(); 171 | if (m_numOwners >= c_maxOwners) return; 172 | m_numOwners++; 173 | m_owners[m_numOwners] = uint256(_owner); 174 | m_ownerIndex[uint256(_owner)] = m_numOwners; 175 | OwnerAdded(_owner); 176 | } 177 | 178 | function removeOwner(address _owner) onlymanyowners(sha3(msg.data)) { 179 | uint256 ownerIndex = m_ownerIndex[uint256(_owner)]; 180 | if (ownerIndex == 0) return; 181 | if (m_required > m_numOwners - 1) return; 182 | 183 | m_owners[ownerIndex] = 0; 184 | m_ownerIndex[uint256(_owner)] = 0; 185 | clearPending(); 186 | reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot 187 | OwnerRemoved(_owner); 188 | } 189 | 190 | function changeRequirement(uint256 _newRequired) 191 | onlymanyowners(sha3(msg.data)) 192 | { 193 | if (_newRequired > m_numOwners) return; 194 | m_required = _newRequired; 195 | clearPending(); 196 | RequirementChanged(_newRequired); 197 | } 198 | 199 | function isOwner(address _addr) returns (bool) { 200 | return m_ownerIndex[uint256(_addr)] > 0; 201 | } 202 | 203 | function hasConfirmed(bytes32 _operation, address _owner) 204 | constant 205 | returns (bool) 206 | { 207 | var pending = m_pending[_operation]; 208 | uint256 ownerIndex = m_ownerIndex[uint256(_owner)]; 209 | 210 | // make sure they're an owner 211 | if (ownerIndex == 0) return false; 212 | 213 | // determine the bit to set for this owner. 214 | uint256 ownerIndexBit = 2**ownerIndex; 215 | return !(pending.ownersDone & ownerIndexBit == 0); 216 | } 217 | 218 | // INTERNAL METHODS 219 | 220 | function confirmAndCheck(bytes32 _operation) internal returns (bool) { 221 | // determine what index the present sender is: 222 | uint256 ownerIndex = m_ownerIndex[uint256(msg.sender)]; 223 | // make sure they're an owner 224 | if (ownerIndex == 0) return; 225 | 226 | var pending = m_pending[_operation]; 227 | // if we're not yet working on this operation, switch over and reset the confirmation status. 228 | if (pending.yetNeeded == 0) { 229 | // reset count of confirmations needed. 230 | pending.yetNeeded = m_required; 231 | // reset which owners have confirmed (none) - set our bitmap to 0. 232 | pending.ownersDone = 0; 233 | pending.index = m_pendingIndex.length++; 234 | m_pendingIndex[pending.index] = _operation; 235 | } 236 | // determine the bit to set for this owner. 237 | uint256 ownerIndexBit = 2**ownerIndex; 238 | // make sure we (the message sender) haven't confirmed this operation previously. 239 | if (pending.ownersDone & ownerIndexBit == 0) { 240 | Confirmation(msg.sender, _operation); 241 | // ok - check if count is enough to go ahead. 242 | if (pending.yetNeeded <= 1) { 243 | // enough confirmations: reset and run interior. 244 | delete m_pendingIndex[m_pending[_operation].index]; 245 | delete m_pending[_operation]; 246 | return true; 247 | } else { 248 | // not enough: record that this owner in particular confirmed. 249 | pending.yetNeeded--; 250 | pending.ownersDone |= ownerIndexBit; 251 | } 252 | } 253 | } 254 | 255 | function reorganizeOwners() private { 256 | uint256 free = 1; 257 | while (free < m_numOwners) { 258 | while (free < m_numOwners && m_owners[free] != 0) free++; 259 | while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; 260 | if ( 261 | free < m_numOwners && 262 | m_owners[m_numOwners] != 0 && 263 | m_owners[free] == 0 264 | ) { 265 | m_owners[free] = m_owners[m_numOwners]; 266 | m_ownerIndex[m_owners[free]] = free; 267 | m_owners[m_numOwners] = 0; 268 | } 269 | } 270 | } 271 | 272 | function clearPending() internal { 273 | uint256 length = m_pendingIndex.length; 274 | for (uint256 i = 0; i < length; ++i) 275 | if (m_pendingIndex[i] != 0) delete m_pending[m_pendingIndex[i]]; 276 | delete m_pendingIndex; 277 | } 278 | 279 | /****************************** 280 | ****** DAY LIMIT SECTION ***** 281 | ******************************/ 282 | 283 | // MODIFIERS 284 | 285 | // simple modifier for daily limit. 286 | modifier limitedDaily(uint256 _value) { 287 | if (underLimit(_value)) _; 288 | } 289 | 290 | // METHODS 291 | 292 | // constructor - stores initial daily limit and records the present day's index. 293 | function initDaylimit(uint256 _limit) { 294 | m_dailyLimit = _limit; 295 | m_lastDay = today(); 296 | } 297 | 298 | // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. 299 | function setDailyLimit(uint256 _newLimit) onlymanyowners(sha3(msg.data)) { 300 | m_dailyLimit = _newLimit; 301 | } 302 | 303 | // resets the amount already spent today. needs many of the owners to confirm. 304 | function resetSpentToday() onlymanyowners(sha3(msg.data)) { 305 | m_spentToday = 0; 306 | } 307 | 308 | // INTERNAL METHODS 309 | 310 | // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and 311 | // returns true. otherwise just returns false. 312 | function underLimit(uint256 _value) internal onlyowner returns (bool) { 313 | // reset the spend limit if we're on a different day to last time. 314 | if (today() > m_lastDay) { 315 | m_spentToday = 0; 316 | m_lastDay = today(); 317 | } 318 | // check to see if there's enough left - if so, subtract and return true. 319 | // overflow protection // dailyLimit check 320 | if ( 321 | m_spentToday + _value >= m_spentToday && 322 | m_spentToday + _value <= m_dailyLimit 323 | ) { 324 | m_spentToday += _value; 325 | return true; 326 | } 327 | return false; 328 | } 329 | 330 | // determines today's index. 331 | function today() private constant returns (uint256) { 332 | return now / 1 days; 333 | } 334 | 335 | /****************************** 336 | ********* WALLET SECTION ***** 337 | ******************************/ 338 | 339 | // METHODS 340 | 341 | // constructor - just pass on the owner array to the multiowned and 342 | // the limit to daylimit 343 | function initWallet( 344 | address[] _owners, 345 | uint256 _required, 346 | uint256 _daylimit 347 | ) { 348 | initMultiowned(_owners, _required); 349 | initDaylimit(_daylimit); 350 | } 351 | 352 | // kills the contract sending everything to `_to`. 353 | function kill(address _to) onlymanyowners(sha3(msg.data)) { 354 | suicide(_to); 355 | } 356 | 357 | // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. 358 | // If not, goes into multisig process. We provide a hash on return to allow the sender to provide 359 | // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value 360 | // and _data arguments). They still get the option of using them if they want, anyways. 361 | function execute( 362 | address _to, 363 | uint256 _value, 364 | bytes _data 365 | ) onlyowner returns (bool _callValue) { 366 | // first, take the opportunity to check that we're under the daily limit. 367 | if (underLimit(_value)) { 368 | SingleTransact(msg.sender, _value, _to, _data); 369 | // yes - just execute the call. 370 | _callValue = _to.call.value(_value)(_data); 371 | } else { 372 | // determine our operation hash. 373 | bytes32 _r = sha3(msg.data, block.number); 374 | if (!confirm(_r) && m_txs[_r].to == 0) { 375 | m_txs[_r].to = _to; 376 | m_txs[_r].value = _value; 377 | m_txs[_r].data = _data; 378 | ConfirmationNeeded(_r, msg.sender, _value, _to, _data); 379 | } 380 | } 381 | } 382 | 383 | // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order 384 | // to determine the body of the transaction from the hash provided. 385 | function confirm(bytes32 _h) onlymanyowners(_h) returns (bool) { 386 | if (m_txs[_h].to != 0) { 387 | m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data); 388 | MultiTransact( 389 | msg.sender, 390 | _h, 391 | m_txs[_h].value, 392 | m_txs[_h].to, 393 | m_txs[_h].data 394 | ); 395 | delete m_txs[_h]; 396 | return true; 397 | } 398 | } 399 | 400 | // INTERNAL METHODS 401 | 402 | function clearWalletPending() internal { 403 | uint256 length = m_pendingIndex.length; 404 | for (uint256 i = 0; i < length; ++i) delete m_txs[m_pendingIndex[i]]; 405 | clearPending(); 406 | } 407 | 408 | // FIELDS 409 | address constant _walletLibrary = 410 | 0x4f2875f631f4fc66b8e051defba0c9f9106d7d5a; 411 | 412 | // the number of owners that must confirm the same operation before it is run. 413 | uint256 m_required; 414 | // pointer used to find a free slot in m_owners 415 | uint256 m_numOwners; 416 | 417 | uint256 public m_dailyLimit; 418 | uint256 public m_spentToday; 419 | uint256 public m_lastDay; 420 | 421 | // list of owners 422 | uint256[256] m_owners; 423 | uint256 constant c_maxOwners = 250; 424 | 425 | // index on the list of owners to allow reverse lookup 426 | mapping(uint256 => uint256) m_ownerIndex; 427 | // the ongoing operations. 428 | mapping(bytes32 => PendingState) m_pending; 429 | bytes32[] m_pendingIndex; 430 | 431 | // pending transactions we have at present. 432 | mapping(bytes32 => Transaction) m_txs; 433 | } 434 | 435 | contract Wallet is multisig { 436 | // WALLET CONSTRUCTOR 437 | // calls the `initWallet` method of the Library in this context 438 | function Wallet( 439 | address[] _owners, 440 | uint256 _required, 441 | uint256 _daylimit 442 | ) { 443 | // Signature of the Wallet Library's init function 444 | bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)")); 445 | address target = _walletLibrary; 446 | 447 | // Compute the size of the call data : arrays has 2 448 | // 32bytes for offset and length, plus 32bytes per element ; 449 | // plus 2 32bytes for each uint 450 | uint256 argarraysize = (2 + _owners.length); 451 | uint256 argsize = (2 + argarraysize) * 32; 452 | 453 | assembly { 454 | // Add the signature first to memory 455 | mstore(0x0, sig) 456 | // Add the call data, which is at the end of the 457 | // code 458 | codecopy(0x4, sub(codesize, argsize), argsize) 459 | // Delegate call to the library 460 | delegatecall( 461 | sub(gas, 10000), 462 | target, 463 | 0x0, 464 | add(argsize, 0x4), 465 | 0x0, 466 | 0x0 467 | ) 468 | } 469 | } 470 | 471 | // METHODS 472 | 473 | // gets called when no other function matches 474 | function() payable { 475 | // just being sent some cash? 476 | if (msg.value > 0) Deposit(msg.sender, msg.value); 477 | else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data); 478 | } 479 | 480 | // Gets an owner by 0-indexed position (using numOwners as the count) 481 | function getOwner(uint256 ownerIndex) constant returns (address) { 482 | return address(m_owners[ownerIndex + 1]); 483 | } 484 | 485 | // As return statement unavailable in fallback, explicit the method here 486 | 487 | function hasConfirmed(bytes32 _operation, address _owner) 488 | constant 489 | returns (bool) 490 | { 491 | return _walletLibrary.delegatecall(msg.data); 492 | } 493 | 494 | function isOwner(address _addr) returns (bool) { 495 | return _walletLibrary.delegatecall(msg.data); 496 | } 497 | 498 | // FIELDS 499 | address constant _walletLibrary = 500 | 0x4f2875f631f4fc66b8e051defba0c9f9106d7d5a; 501 | 502 | // the number of owners that must confirm the same operation before it is run. 503 | uint256 public m_required; 504 | // pointer used to find a free slot in m_owners 505 | uint256 public m_numOwners; 506 | 507 | uint256 public m_dailyLimit; 508 | uint256 public m_spentToday; 509 | uint256 public m_lastDay; 510 | 511 | // list of owners 512 | uint256[256] m_owners; 513 | } 514 | -------------------------------------------------------------------------------- /MultiSig/img/attacker_txs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/MultiSig/img/attacker_txs.png -------------------------------------------------------------------------------- /MultiSig/img/execute_tx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/MultiSig/img/execute_tx.png -------------------------------------------------------------------------------- /MultiSig/img/init_tx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/MultiSig/img/init_tx.png -------------------------------------------------------------------------------- /ParityBug/Analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis of the Parity Bug 2 | 3 | After the MultiSig attack of July 2017 (where anyone could reinitialize any multisig wallet thanks to `initWallet()`, become owner, and withdraw all funds), the Parity team deployed their new Parity wallet library with the previous vulnerability "fixed": 4 | ```js 5 | // constructor - just pass on the owner array to the multiowned and 6 | // the limit to daylimit 7 | function initWallet( 8 | address[] _owners, 9 | uint256 _required, 10 | uint256 _daylimit 11 | ) only_uninitialized { // can only be initialized once and since this function was called in the *Wallet* constructor, they thought they were safe 12 | initDaylimit(_daylimit); 13 | initMultiowned(_owners, _required); 14 | } 15 | 16 | ``` 17 | A few month later, a user "accidentally" managed to suicide the library contract by calling the very same `initWallet()` function, since there had been no initialization on the library contract. 18 | 19 | ## Addresses involved 20 | The exploited *Library* contract i.e. ParityBug: Trigger: [0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4](https://etherscan.io/address/0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4#code) 21 | Polkadot: MultiSig, one of the victim *Wallet*: [0x3BfC20f0B9aFcAcE800D73D2191166FF16540258](https://etherscan.io/address/0x3bfc20f0b9afcace800d73d2191166ff16540258#code) 22 | The initWallet tx: [0x05f71e1b2cb4f03e547739db15d080fd30c989eda04d37ce6264c5686e0722c9](https://etherscan.io/tx/0x05f71e1b2cb4f03e547739db15d080fd30c989eda04d37ce6264c5686e0722c9) 23 | The selfdestruct tx: [0x47f7cff7a5e671884629c93b368cb18f58a993f4b19c2a53a8662e3f1482f690](https://etherscan.io/tx/0x47f7cff7a5e671884629c93b368cb18f58a993f4b19c2a53a8662e3f1482f690) 24 | 25 | The attacker address: [0xae7168deb525862f4fee37d987a971b385b96952](https://etherscan.io/address/0xae7168deb525862f4fee37d987a971b385b96952) 26 | 27 | [153 victim contractss on Etherscan](https://etherscan.io/accounts/label/parity-bug) 28 | 29 | ## The vulnerability: Uninitialization 30 | 31 | The main vulnerability again involve `initWallet()`, only now it can only be called once. The function was correctly written, from the point of view of the Wallet contract, to make it uncallable after been initialized once. But since it was never called on the library contract itself, any one could call it and become owner of the **library**. Becoming owner of the library is not very interesting, you can change the library storage a bit but that is all. 32 | The big problem comes from the fact that the contract was a library, which was used by a lot (500+) of different Parity Wallet smart contract. If this library were to disappear (become uncallable) those contracts who rely on it for their logic would simply become unusable. 33 | The library have a `kill()` function that call `selfdestruct()` (`suicide()`). It is protected by ownership, but since `initWallet()` can give ownership to anyone, the first user to call `initWallet()` can now destroy the library and all attached contracts. 34 | 35 | When the library gets killed, all contracts whose functions `delegatecall()` to it will no longer work. Since, in the Parity Wallet, the library address is a constant variable and thus hardcoded in the bytecode of the contract once deploy: 36 | ```js 37 | address constant _walletLibrary = 0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4; 38 | ``` 39 | those contracts become completely unusable. 40 | 41 | 42 | 43 | ## The exploit 44 | 45 | The attacker first called `initWallet([attacker_addr],0,0)` 46 | 47 | ![initWallet transaction](ParityBug/img/initWallet.png "initWallet transaction") 48 | 49 | which was never(!) called before, 50 | `initWallet()`: 51 | ```js 52 | // throw unless the contract is not yet initialized. 53 | modifier only_uninitialized() { 54 | if (m_numOwners > 0) throw; 55 | _; 56 | } 57 | 58 | function initWallet( 59 | address[] _owners, 60 | uint256 _required, 61 | uint256 _daylimit 62 | ) only_uninitialized { //modifier doesn't throw since m_numOwners on the library storage was equal to zero 63 | initDaylimit(_daylimit); 64 | initMultiowned(_owners, _required); // attacker becomes owner 65 | } 66 | 67 | function initMultiowned(address[] _owners, uint256 _required) 68 | only_uninitialized 69 | { 70 | m_numOwners = _owners.length + 1; 71 | m_owners[1] = uint256(msg.sender); 72 | m_ownerIndex[uint256(msg.sender)] = 1; 73 | for (uint256 i = 0; i < _owners.length; ++i) { 74 | m_owners[2 + i] = uint256(_owners[i]); 75 | m_ownerIndex[uint256(_owners[i])] = 2 + i; 76 | } 77 | m_required = _required; 78 | } 79 | ``` 80 | This allowed him to become owner of the library, he could then call `kill()` on the library: 81 | 82 | ![Kill transaction](img/kill.png "Kill transaction") 83 | 84 | ```js 85 | // kills the contract sending everything to `_to`. 86 | function kill(address _to) external onlymanyowners(sha3(msg.data)) { // onlymanyowners modifier 87 | suicide(_to); 88 | } 89 | 90 | modifier onlymanyowners(bytes32 _operation) { 91 | if (confirmAndCheck(_operation)) _; 92 | } 93 | 94 | function confirmAndCheck(bytes32 _operation) internal returns (bool) { 95 | // determine what index the present sender is: 96 | uint256 ownerIndex = m_ownerIndex[uint256(msg.sender)]; //** set in initMultiowned 97 | // make sure they're an owner 98 | if (ownerIndex == 0) return; // ** false 99 | 100 | ... // ** nothing much happen after that, the function finishes and return true 101 | } 102 | ``` 103 | 104 | The library is now gone and all contracts whose functions `delegatecall()` to it will no longer work. 105 | 106 | The attacker then open an issue on Github to report his findings: 107 | 108 | ![Issue on GitHub](img/github.png "Issue on GitHub") 109 | 110 | 111 | 112 | ## The Aftermath 113 | 114 | Parity: "All dependent multi-sig wallets that were deployed after 20th July functionally now look as follows: 115 | ```solidity 116 | contract Wallet { 117 | function () payable { 118 | Deposit(...) 119 | } 120 | } 121 | ``` 122 | This means that currently no funds can be moved out of the multi-sig wallets." 123 | 124 | Overall over 500,000 ETH in more than 500 different wallet have been lost, stuck on these contract and completely unaccessible. The biggest lose was [Polkadot](https://etherscan.io/address/0x3bfc20f0b9afcace800d73d2191166ff16540258#code) who lost over 300,000 ETH. 125 | 126 | 127 | ## Sources 128 | Parity blog post: 129 | [Security Alert](https://www.parity.io/blog/security-alert) 130 | [Security Alert 2](https://www.parity.io/blog/security-alert-2/) 131 | [Parity Technologies Multi-Sig Wallet Issue Update](https://www.parity.io/blog/parity-technologies-multi-sig-wallet-issue-update/) 132 | [A Postmortem on the Parity Multi-Sig Library Self-Destruct](https://www.parity.io/blog/a-postmortem-on-the-parity-multi-sig-library-self-destruct/) 133 | [On Classes of Stuck Ether and Potential Solutions](https://www.parity.io/blog/on-classes-of-stuck-ether-and-potential-solutions/) 134 | 135 | [Github issue first mentioning the problem](https://github.com/openethereum/parity-ethereum/issues/6995) -------------------------------------------------------------------------------- /ParityBug/WalletLibrary.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2017-07-20 3 | */ 4 | 5 | //sol Wallet 6 | // Multi-sig, daily-limited account proxy/wallet. 7 | // @authors: 8 | // Gav Wood 9 | // inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a 10 | // single, or, crucially, each of a number of, designated owners. 11 | // usage: 12 | // use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by 13 | // some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the 14 | // interior is executed. 15 | 16 | pragma solidity ^0.4.9; 17 | 18 | contract WalletEvents { 19 | // EVENTS 20 | 21 | // this contract only has six types of events: it can accept a confirmation, in which case 22 | // we record owner and operation (hash) alongside it. 23 | event Confirmation(address owner, bytes32 operation); 24 | event Revoke(address owner, bytes32 operation); 25 | 26 | // some others are in the case of an owner changing. 27 | event OwnerChanged(address oldOwner, address newOwner); 28 | event OwnerAdded(address newOwner); 29 | event OwnerRemoved(address oldOwner); 30 | 31 | // the last one is emitted if the required signatures change 32 | event RequirementChanged(uint256 newRequirement); 33 | 34 | // Funds has arrived into the wallet (record how much). 35 | event Deposit(address _from, uint256 value); 36 | // Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going). 37 | event SingleTransact( 38 | address owner, 39 | uint256 value, 40 | address to, 41 | bytes data, 42 | address created 43 | ); 44 | // Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going). 45 | event MultiTransact( 46 | address owner, 47 | bytes32 operation, 48 | uint256 value, 49 | address to, 50 | bytes data, 51 | address created 52 | ); 53 | // Confirmation still needed for a transaction. 54 | event ConfirmationNeeded( 55 | bytes32 operation, 56 | address initiator, 57 | uint256 value, 58 | address to, 59 | bytes data 60 | ); 61 | } 62 | 63 | contract WalletAbi { 64 | // Revokes a prior confirmation of the given operation 65 | function revoke(bytes32 _operation) external; 66 | 67 | // Replaces an owner `_from` with another `_to`. 68 | function changeOwner(address _from, address _to) external; 69 | 70 | function addOwner(address _owner) external; 71 | 72 | function removeOwner(address _owner) external; 73 | 74 | function changeRequirement(uint256 _newRequired) external; 75 | 76 | function isOwner(address _addr) constant returns (bool); 77 | 78 | function hasConfirmed(bytes32 _operation, address _owner) 79 | external 80 | constant 81 | returns (bool); 82 | 83 | // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. 84 | function setDailyLimit(uint256 _newLimit) external; 85 | 86 | function execute( 87 | address _to, 88 | uint256 _value, 89 | bytes _data 90 | ) external returns (bytes32 o_hash); 91 | 92 | function confirm(bytes32 _h) returns (bool o_success); 93 | } 94 | 95 | contract WalletLibrary is WalletEvents { 96 | // TYPES 97 | 98 | // struct for the status of a pending operation. 99 | struct PendingState { 100 | uint256 yetNeeded; 101 | uint256 ownersDone; 102 | uint256 index; 103 | } 104 | 105 | // Transaction structure to remember details of transaction lest it need be saved for a later call. 106 | struct Transaction { 107 | address to; 108 | uint256 value; 109 | bytes data; 110 | } 111 | 112 | // MODIFIERS 113 | 114 | // simple single-sig function modifier. 115 | modifier onlyowner() { 116 | if (isOwner(msg.sender)) _; 117 | } 118 | // multi-sig function modifier: the operation must have an intrinsic hash in order 119 | // that later attempts can be realised as the same underlying operation and 120 | // thus count as confirmations. 121 | modifier onlymanyowners(bytes32 _operation) { 122 | if (confirmAndCheck(_operation)) _; 123 | } 124 | 125 | // METHODS 126 | 127 | // gets called when no other function matches 128 | function() payable { 129 | // just being sent some cash? 130 | if (msg.value > 0) Deposit(msg.sender, msg.value); 131 | } 132 | 133 | // constructor is given number of sigs required to do protected "onlymanyowners" transactions 134 | // as well as the selection of addresses capable of confirming them. 135 | function initMultiowned(address[] _owners, uint256 _required) 136 | only_uninitialized 137 | { 138 | m_numOwners = _owners.length + 1; 139 | m_owners[1] = uint256(msg.sender); 140 | m_ownerIndex[uint256(msg.sender)] = 1; 141 | for (uint256 i = 0; i < _owners.length; ++i) { 142 | m_owners[2 + i] = uint256(_owners[i]); 143 | m_ownerIndex[uint256(_owners[i])] = 2 + i; 144 | } 145 | m_required = _required; 146 | } 147 | 148 | // Revokes a prior confirmation of the given operation 149 | function revoke(bytes32 _operation) external { 150 | uint256 ownerIndex = m_ownerIndex[uint256(msg.sender)]; 151 | // make sure they're an owner 152 | if (ownerIndex == 0) return; 153 | uint256 ownerIndexBit = 2**ownerIndex; 154 | var pending = m_pending[_operation]; 155 | if (pending.ownersDone & ownerIndexBit > 0) { 156 | pending.yetNeeded++; 157 | pending.ownersDone -= ownerIndexBit; 158 | Revoke(msg.sender, _operation); 159 | } 160 | } 161 | 162 | // Replaces an owner `_from` with another `_to`. 163 | function changeOwner(address _from, address _to) 164 | external 165 | onlymanyowners(sha3(msg.data)) 166 | { 167 | if (isOwner(_to)) return; 168 | uint256 ownerIndex = m_ownerIndex[uint256(_from)]; 169 | if (ownerIndex == 0) return; 170 | 171 | clearPending(); 172 | m_owners[ownerIndex] = uint256(_to); 173 | m_ownerIndex[uint256(_from)] = 0; 174 | m_ownerIndex[uint256(_to)] = ownerIndex; 175 | OwnerChanged(_from, _to); 176 | } 177 | 178 | function addOwner(address _owner) external onlymanyowners(sha3(msg.data)) { 179 | if (isOwner(_owner)) return; 180 | 181 | clearPending(); 182 | if (m_numOwners >= c_maxOwners) reorganizeOwners(); 183 | if (m_numOwners >= c_maxOwners) return; 184 | m_numOwners++; 185 | m_owners[m_numOwners] = uint256(_owner); 186 | m_ownerIndex[uint256(_owner)] = m_numOwners; 187 | OwnerAdded(_owner); 188 | } 189 | 190 | function removeOwner(address _owner) 191 | external 192 | onlymanyowners(sha3(msg.data)) 193 | { 194 | uint256 ownerIndex = m_ownerIndex[uint256(_owner)]; 195 | if (ownerIndex == 0) return; 196 | if (m_required > m_numOwners - 1) return; 197 | 198 | m_owners[ownerIndex] = 0; 199 | m_ownerIndex[uint256(_owner)] = 0; 200 | clearPending(); 201 | reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot 202 | OwnerRemoved(_owner); 203 | } 204 | 205 | function changeRequirement(uint256 _newRequired) 206 | external 207 | onlymanyowners(sha3(msg.data)) 208 | { 209 | if (_newRequired > m_numOwners) return; 210 | m_required = _newRequired; 211 | clearPending(); 212 | RequirementChanged(_newRequired); 213 | } 214 | 215 | // Gets an owner by 0-indexed position (using numOwners as the count) 216 | function getOwner(uint256 ownerIndex) external constant returns (address) { 217 | return address(m_owners[ownerIndex + 1]); 218 | } 219 | 220 | function isOwner(address _addr) constant returns (bool) { 221 | return m_ownerIndex[uint256(_addr)] > 0; 222 | } 223 | 224 | function hasConfirmed(bytes32 _operation, address _owner) 225 | external 226 | constant 227 | returns (bool) 228 | { 229 | var pending = m_pending[_operation]; 230 | uint256 ownerIndex = m_ownerIndex[uint256(_owner)]; 231 | 232 | // make sure they're an owner 233 | if (ownerIndex == 0) return false; 234 | 235 | // determine the bit to set for this owner. 236 | uint256 ownerIndexBit = 2**ownerIndex; 237 | return !(pending.ownersDone & ownerIndexBit == 0); 238 | } 239 | 240 | // constructor - stores initial daily limit and records the present day's index. 241 | function initDaylimit(uint256 _limit) only_uninitialized { 242 | m_dailyLimit = _limit; 243 | m_lastDay = today(); 244 | } 245 | 246 | // (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today. 247 | function setDailyLimit(uint256 _newLimit) 248 | external 249 | onlymanyowners(sha3(msg.data)) 250 | { 251 | m_dailyLimit = _newLimit; 252 | } 253 | 254 | // resets the amount already spent today. needs many of the owners to confirm. 255 | function resetSpentToday() external onlymanyowners(sha3(msg.data)) { 256 | m_spentToday = 0; 257 | } 258 | 259 | // throw unless the contract is not yet initialized. 260 | modifier only_uninitialized() { 261 | if (m_numOwners > 0) throw; 262 | _; 263 | } 264 | 265 | // constructor - just pass on the owner array to the multiowned and 266 | // the limit to daylimit 267 | function initWallet( 268 | address[] _owners, 269 | uint256 _required, 270 | uint256 _daylimit 271 | ) only_uninitialized { 272 | initDaylimit(_daylimit); 273 | initMultiowned(_owners, _required); 274 | } 275 | 276 | // kills the contract sending everything to `_to`. 277 | function kill(address _to) external onlymanyowners(sha3(msg.data)) { 278 | suicide(_to); 279 | } 280 | 281 | // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. 282 | // If not, goes into multisig process. We provide a hash on return to allow the sender to provide 283 | // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value 284 | // and _data arguments). They still get the option of using them if they want, anyways. 285 | function execute( 286 | address _to, 287 | uint256 _value, 288 | bytes _data 289 | ) external onlyowner returns (bytes32 o_hash) { 290 | // first, take the opportunity to check that we're under the daily limit. 291 | if ((_data.length == 0 && underLimit(_value)) || m_required == 1) { 292 | // yes - just execute the call. 293 | address created; 294 | if (_to == 0) { 295 | created = create(_value, _data); 296 | } else { 297 | if (!_to.call.value(_value)(_data)) throw; 298 | } 299 | SingleTransact(msg.sender, _value, _to, _data, created); 300 | } else { 301 | // determine our operation hash. 302 | o_hash = sha3(msg.data, block.number); 303 | // store if it's new 304 | if ( 305 | m_txs[o_hash].to == 0 && 306 | m_txs[o_hash].value == 0 && 307 | m_txs[o_hash].data.length == 0 308 | ) { 309 | m_txs[o_hash].to = _to; 310 | m_txs[o_hash].value = _value; 311 | m_txs[o_hash].data = _data; 312 | } 313 | if (!confirm(o_hash)) { 314 | ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data); 315 | } 316 | } 317 | } 318 | 319 | function create(uint256 _value, bytes _code) 320 | internal 321 | returns (address o_addr) 322 | { 323 | assembly { 324 | o_addr := create(_value, add(_code, 0x20), mload(_code)) 325 | jumpi(invalidJumpLabel, iszero(extcodesize(o_addr))) 326 | } 327 | } 328 | 329 | // confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order 330 | // to determine the body of the transaction from the hash provided. 331 | function confirm(bytes32 _h) onlymanyowners(_h) returns (bool o_success) { 332 | if ( 333 | m_txs[_h].to != 0 || 334 | m_txs[_h].value != 0 || 335 | m_txs[_h].data.length != 0 336 | ) { 337 | address created; 338 | if (m_txs[_h].to == 0) { 339 | created = create(m_txs[_h].value, m_txs[_h].data); 340 | } else { 341 | if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data)) 342 | throw; 343 | } 344 | 345 | MultiTransact( 346 | msg.sender, 347 | _h, 348 | m_txs[_h].value, 349 | m_txs[_h].to, 350 | m_txs[_h].data, 351 | created 352 | ); 353 | delete m_txs[_h]; 354 | return true; 355 | } 356 | } 357 | 358 | // INTERNAL METHODS 359 | 360 | function confirmAndCheck(bytes32 _operation) internal returns (bool) { 361 | // determine what index the present sender is: 362 | uint256 ownerIndex = m_ownerIndex[uint256(msg.sender)]; 363 | // make sure they're an owner 364 | if (ownerIndex == 0) return; 365 | 366 | var pending = m_pending[_operation]; 367 | // if we're not yet working on this operation, switch over and reset the confirmation status. 368 | if (pending.yetNeeded == 0) { 369 | // reset count of confirmations needed. 370 | pending.yetNeeded = m_required; 371 | // reset which owners have confirmed (none) - set our bitmap to 0. 372 | pending.ownersDone = 0; 373 | pending.index = m_pendingIndex.length++; 374 | m_pendingIndex[pending.index] = _operation; 375 | } 376 | // determine the bit to set for this owner. 377 | uint256 ownerIndexBit = 2**ownerIndex; 378 | // make sure we (the message sender) haven't confirmed this operation previously. 379 | if (pending.ownersDone & ownerIndexBit == 0) { 380 | Confirmation(msg.sender, _operation); 381 | // ok - check if count is enough to go ahead. 382 | if (pending.yetNeeded <= 1) { 383 | // enough confirmations: reset and run interior. 384 | delete m_pendingIndex[m_pending[_operation].index]; 385 | delete m_pending[_operation]; 386 | return true; 387 | } else { 388 | // not enough: record that this owner in particular confirmed. 389 | pending.yetNeeded--; 390 | pending.ownersDone |= ownerIndexBit; 391 | } 392 | } 393 | } 394 | 395 | function reorganizeOwners() private { 396 | uint256 free = 1; 397 | while (free < m_numOwners) { 398 | while (free < m_numOwners && m_owners[free] != 0) free++; 399 | while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--; 400 | if ( 401 | free < m_numOwners && 402 | m_owners[m_numOwners] != 0 && 403 | m_owners[free] == 0 404 | ) { 405 | m_owners[free] = m_owners[m_numOwners]; 406 | m_ownerIndex[m_owners[free]] = free; 407 | m_owners[m_numOwners] = 0; 408 | } 409 | } 410 | } 411 | 412 | // checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and 413 | // returns true. otherwise just returns false. 414 | function underLimit(uint256 _value) internal onlyowner returns (bool) { 415 | // reset the spend limit if we're on a different day to last time. 416 | if (today() > m_lastDay) { 417 | m_spentToday = 0; 418 | m_lastDay = today(); 419 | } 420 | // check to see if there's enough left - if so, subtract and return true. 421 | // overflow protection // dailyLimit check 422 | if ( 423 | m_spentToday + _value >= m_spentToday && 424 | m_spentToday + _value <= m_dailyLimit 425 | ) { 426 | m_spentToday += _value; 427 | return true; 428 | } 429 | return false; 430 | } 431 | 432 | // determines today's index. 433 | function today() private constant returns (uint256) { 434 | return now / 1 days; 435 | } 436 | 437 | function clearPending() internal { 438 | uint256 length = m_pendingIndex.length; 439 | 440 | for (uint256 i = 0; i < length; ++i) { 441 | delete m_txs[m_pendingIndex[i]]; 442 | 443 | if (m_pendingIndex[i] != 0) delete m_pending[m_pendingIndex[i]]; 444 | } 445 | 446 | delete m_pendingIndex; 447 | } 448 | 449 | // FIELDS 450 | address constant _walletLibrary = 451 | 0xcafecafecafecafecafecafecafecafecafecafe; 452 | 453 | // the number of owners that must confirm the same operation before it is run. 454 | uint256 public m_required; 455 | // pointer used to find a free slot in m_owners 456 | uint256 public m_numOwners; 457 | 458 | uint256 public m_dailyLimit; 459 | uint256 public m_spentToday; 460 | uint256 public m_lastDay; 461 | 462 | // list of owners 463 | uint256[256] m_owners; 464 | 465 | uint256 constant c_maxOwners = 250; 466 | // index on the list of owners to allow reverse lookup 467 | mapping(uint256 => uint256) m_ownerIndex; 468 | // the ongoing operations. 469 | mapping(bytes32 => PendingState) m_pending; 470 | bytes32[] m_pendingIndex; 471 | 472 | // pending transactions we have at present. 473 | mapping(bytes32 => Transaction) m_txs; 474 | } 475 | 476 | contract Wallet is WalletEvents { 477 | // WALLET CONSTRUCTOR 478 | // calls the `initWallet` method of the Library in this context 479 | function Wallet( 480 | address[] _owners, 481 | uint256 _required, 482 | uint256 _daylimit 483 | ) { 484 | // Signature of the Wallet Library's init function 485 | bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)")); 486 | address target = _walletLibrary; 487 | 488 | // Compute the size of the call data : arrays has 2 489 | // 32bytes for offset and length, plus 32bytes per element ; 490 | // plus 2 32bytes for each uint 491 | uint256 argarraysize = (2 + _owners.length); 492 | uint256 argsize = (2 + argarraysize) * 32; 493 | 494 | assembly { 495 | // Add the signature first to memory 496 | mstore(0x0, sig) 497 | // Add the call data, which is at the end of the 498 | // code 499 | codecopy(0x4, sub(codesize, argsize), argsize) 500 | // Delegate call to the library 501 | delegatecall( 502 | sub(gas, 10000), 503 | target, 504 | 0x0, 505 | add(argsize, 0x4), 506 | 0x0, 507 | 0x0 508 | ) 509 | } 510 | } 511 | 512 | // METHODS 513 | 514 | // gets called when no other function matches 515 | function() payable { 516 | // just being sent some cash? 517 | if (msg.value > 0) Deposit(msg.sender, msg.value); 518 | else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data); 519 | } 520 | 521 | // Gets an owner by 0-indexed position (using numOwners as the count) 522 | function getOwner(uint256 ownerIndex) constant returns (address) { 523 | return address(m_owners[ownerIndex + 1]); 524 | } 525 | 526 | // As return statement unavailable in fallback, explicit the method here 527 | 528 | function hasConfirmed(bytes32 _operation, address _owner) 529 | external 530 | constant 531 | returns (bool) 532 | { 533 | return _walletLibrary.delegatecall(msg.data); 534 | } 535 | 536 | function isOwner(address _addr) constant returns (bool) { 537 | return _walletLibrary.delegatecall(msg.data); 538 | } 539 | 540 | // FIELDS 541 | address constant _walletLibrary = 542 | 0xcafecafecafecafecafecafecafecafecafecafe; 543 | 544 | // the number of owners that must confirm the same operation before it is run. 545 | uint256 public m_required; 546 | // pointer used to find a free slot in m_owners 547 | uint256 public m_numOwners; 548 | 549 | uint256 public m_dailyLimit; 550 | uint256 public m_spentToday; 551 | uint256 public m_lastDay; 552 | 553 | // list of owners 554 | uint256[256] m_owners; 555 | } 556 | -------------------------------------------------------------------------------- /ParityBug/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/ParityBug/img/github.png -------------------------------------------------------------------------------- /ParityBug/img/initWallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/ParityBug/img/initWallet.png -------------------------------------------------------------------------------- /ParityBug/img/kill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/ParityBug/img/kill.png -------------------------------------------------------------------------------- /ProofofWeakHandsCoin/Analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis of the Proof of Weak Hands Coin ponzi hack 2 | 4chan decided to create a crypto ponzi scheme, which was advertised as such, and obviously it worked well. In only three days it had over 1,000 ETH, but an underflow vulnerability allowed someone to withdraw 866 ETH from the ponzi. 3 | 4 | ## Addresses involved 5 | The Ponzi contract: [0xa7ca36f7273d4d38fc2aec5a454c497f86728a7a](https://etherscan.io/address/0xa7ca36f7273d4d38fc2aec5a454c497f86728a7a#code) 6 | The approve tx: [0xc836c7c6ac8135cf1df3da5754cfa9959d327e2ec3748f83124089dfe621a98b](https://etherscan.io/tx/0xc836c7c6ac8135cf1df3da5754cfa9959d327e2ec3748f83124089dfe621a98b) 7 | The transferFrom tx: [0x233107922bed72a4ea7c75a83ecf58dae4b744384e2b3feacd28903a17b864e0](https://etherscan.io/tx/0x233107922bed72a4ea7c75a83ecf58dae4b744384e2b3feacd28903a17b864e0) 8 | The withdraw tx: [0x496c0411f52978dfd7953b7e6965465977162bfaf7b88c0c78fcdc97cd395d62](https://etherscan.io/tx/0x496c0411f52978dfd7953b7e6965465977162bfaf7b88c0c78fcdc97cd395d62) 9 | 10 | The attacker address: [0xB9cd700b8A16069Bf77edEdC71c3284780422774](https://etherscan.io/address/0xB9cd700b8A16069Bf77edEdC71c3284780422774) 11 | The attacker 2nd address: [0x945C84b2FdD331ed3E8e7865E830626e6CeFAB94](https://etherscan.io/address/0x945C84b2FdD331ed3E8e7865E830626e6CeFAB94) 12 | 13 | ## The vulnerability: Underflow 14 | 15 | There is two issues in this contract: an underflow vulnerability and a poor internal logic that can potentialy lead to this underflow. 16 | The underflow vulnerability of the contract is in the `sell()` function: 17 | ```solidity 18 | function sell(uint256 amount) internal { 19 | var numEthers = getEtherForTokens(amount); 20 | // remove tokens 21 | totalSupply -= amount; 22 | balanceOfOld[msg.sender] -= amount; //** possible underflow if amount is bigger that the balance 23 | 24 | // fix payouts and put the ethers in payout 25 | var payoutDiff = (int256)( 26 | earningsPerShare * amount + (numEthers * PRECISION) 27 | ); 28 | payouts[msg.sender] -= payoutDiff; 29 | totalPayouts -= payoutDiff; 30 | } 31 | ``` 32 | `sell()` is called when a user call `tranfer()` with `_to` equal to the contract address. There is in fact a check to ensure that when a user wants to sell his tokens, he has that amount of tokens. But since `transferTokens(_from, _to, _value)` allows a user to transfer tokens from a different address (who has previously approved the user to do so), the check is done for that `_from` address and if that address indeed has those tokens, the check pass. But when `sell()` decrease the balance, it does so to the `msg.sender` address instead of the `_from` so if the user (`msg.sender`) has less tokens that the amount passed in `_value`, an underflow will occur. The error is that `_from` is not passed to `sell()` as an argument. 33 | 34 | ```solidity 35 | function transfer(address _to, uint256 _value) public { 36 | transferTokens(msg.sender, _to, _value); 37 | } 38 | 39 | function transferTokens( 40 | address _from, 41 | address _to, 42 | uint256 _value 43 | ) internal { 44 | if (balanceOfOld[_from] < _value) revert(); //** underflow check 45 | if (_to == address(this)) { 46 | sell(_value); //** sell doesn't know who _from is 47 | } else { 48 | int256 payoutDiff = (int256)(earningsPerShare * _value); 49 | balanceOfOld[_from] -= _value; 50 | balanceOfOld[_to] += _value; 51 | payouts[_from] -= payoutDiff; 52 | payouts[_to] += payoutDiff; 53 | } 54 | Transfer(_from, _to, _value); 55 | } 56 | 57 | 58 | ``` 59 | 60 | 61 | ## The exploit 62 | 63 | The attacker will use an underflow to increase his token balance to the maximum amount and will then use that balance to exit the Ponzi with a lot of ETH. 64 | 65 | He first used a second account (0x945) to buy some tokens and to approv his first account (0xb9cd) to transfer some of his tokens: 66 | 67 | ![approve transaction](img/approve.png "approve transaction") 68 | 69 | Now that he can transfer tokens on behalf of his other account, he called `transferFrom(_from_=0x945, _to=PonziContract, _value=1)`: 70 | 71 | ![transferFrom transaction](img/transferFrom.png "transferFrom transaction") 72 | 73 | This check the allowance and then call `transferToken()`: 74 | ```solidity 75 | function transferFrom( 76 | address _from, 77 | address _to, 78 | uint256 _value 79 | ) public { 80 | var _allowance = allowance[_from][msg.sender]; 81 | if (_allowance < _value) revert(); 82 | allowance[_from][msg.sender] = _allowance - _value; 83 | transferTokens(_from, _to, _value); 84 | } 85 | function transferTokens( 86 | address _from, 87 | address _to, 88 | uint256 _value 89 | ) internal { 90 | if (balanceOfOld[_from] < _value) revert(); 91 | if (_to == address(this)) { 92 | sell(_value); //** _from is not passed as parameter 93 | } else { 94 | int256 payoutDiff = (int256)(earningsPerShare * _value); 95 | balanceOfOld[_from] -= _value; 96 | balanceOfOld[_to] += _value; 97 | payouts[_from] -= payoutDiff; 98 | payouts[_to] += payoutDiff; 99 | } 100 | Transfer(_from, _to, _value); 101 | } 102 | ``` 103 | `transferToken()` check that the balance of `_from` is high enough to transfer `_value` of tokens (it is) and since `_to` is the address of the contract, call `sell()`: 104 | ```solidity 105 | function sell(uint256 amount) internal { 106 | var numEthers = getEtherForTokens(amount); 107 | // remove tokens 108 | totalSupply -= amount; 109 | balanceOfOld[msg.sender] -= amount; //** possible underflow if amount is bigger that the balance 110 | 111 | // fix payouts and put the ethers in payout 112 | var payoutDiff = (int256)( 113 | earningsPerShare * amount + (numEthers * PRECISION) 114 | ); 115 | payouts[msg.sender] -= payoutDiff; 116 | totalPayouts -= payoutDiff; 117 | } 118 | ``` 119 | But since `sell()` doesn't receive the `_from` parameter (0x945), it assumes that it is the `msg.sender` (0xb9cd), which doesn't have any token, so `balanceOfOld[msg.sender] -= amount` underflow and is now equal to the maximum amount of `uint256`. 120 | 121 | He then sold some of his tokens and called `withdraw()` which allowed him to exit the Ponzi with over 866 ETH: 122 | ![transfer transaction](img/transfer.png "transfer transaction") 123 | 124 | ![withdraw transaction](img/withdraw.png "withdraw transaction") 125 | 126 | -------------------------------------------------------------------------------- /ProofofWeakHandsCoin/PonziTokenV3.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2018-01-28 3 | */ 4 | 5 | pragma solidity ^0.4.18; 6 | 7 | // If you wanna escape this contract REALLY FAST 8 | // 1. open MEW/METAMASK 9 | // 2. Put this as data: 0xb1e35242 10 | // 3. send 150000+ gas 11 | // That calls the getMeOutOfHere() method 12 | 13 | // Wacky version, 0-1 tokens takes 10eth (should be avg 200% gains), 1-2 takes another 30eth (avg 100% gains), and beyond that who the fuck knows but it's 50% gains 14 | // 10% fees, price goes up crazy fast 15 | contract PonziTokenV3 { 16 | uint256 constant PRECISION = 0x10000000000000000; // 2^64 17 | // CRR = 80 % 18 | int256 constant CRRN = 1; 19 | int256 constant CRRD = 2; 20 | // The price coefficient. Chosen such that at 1 token total supply 21 | // the reserve is 0.8 ether and price 1 ether/token. 22 | int256 constant LOGC = -0x296ABF784A358468C; 23 | 24 | string public constant name = "ProofOfWeakHands"; 25 | string public constant symbol = "POWH"; 26 | uint8 public constant decimals = 18; 27 | uint256 public totalSupply; 28 | // amount of shares for each address (scaled number) 29 | mapping(address => uint256) public balanceOfOld; 30 | // allowance map, see erc20 31 | mapping(address => mapping(address => uint256)) public allowance; 32 | // amount payed out for each address (scaled number) 33 | mapping(address => int256) payouts; 34 | // sum of all payouts (scaled number) 35 | int256 totalPayouts; 36 | // amount earned for each share (scaled number) 37 | uint256 earningsPerShare; 38 | 39 | event Transfer(address indexed from, address indexed to, uint256 value); 40 | event Approval( 41 | address indexed owner, 42 | address indexed spender, 43 | uint256 value 44 | ); 45 | 46 | //address owner; 47 | 48 | function PonziTokenV3() public { 49 | //owner = msg.sender; 50 | } 51 | 52 | // These are functions solely created to appease the frontend 53 | function balanceOf(address _owner) 54 | public 55 | constant 56 | returns (uint256 balance) 57 | { 58 | return balanceOfOld[_owner]; 59 | } 60 | 61 | function withdraw( 62 | uint256 tokenCount // the parameter is ignored, yes 63 | ) public returns (bool) { 64 | var balance = dividends(msg.sender); 65 | payouts[msg.sender] += (int256)(balance * PRECISION); 66 | totalPayouts += (int256)(balance * PRECISION); 67 | msg.sender.transfer(balance); 68 | return true; 69 | } 70 | 71 | function sellMyTokensDaddy() public { 72 | var balance = balanceOf(msg.sender); 73 | transferTokens(msg.sender, address(this), balance); // this triggers the internal sell function 74 | } 75 | 76 | function getMeOutOfHere() public { 77 | sellMyTokensDaddy(); 78 | withdraw(1); // parameter is ignored 79 | } 80 | 81 | function fund() public payable returns (bool) { 82 | if (msg.value > 0.000001 ether) buy(); 83 | else return false; 84 | 85 | return true; 86 | } 87 | 88 | function buyPrice() public constant returns (uint256) { 89 | return getTokensForEther(1 finney); 90 | } 91 | 92 | function sellPrice() public constant returns (uint256) { 93 | return getEtherForTokens(1 finney); 94 | } 95 | 96 | // End of useless functions 97 | 98 | // Invariants 99 | // totalPayout/Supply correct: 100 | // totalPayouts = \sum_{addr:address} payouts(addr) 101 | // totalSupply = \sum_{addr:address} balanceOfOld(addr) 102 | // dividends not negative: 103 | // \forall addr:address. payouts[addr] <= earningsPerShare * balanceOfOld[addr] 104 | // supply/reserve correlation: 105 | // totalSupply ~= exp(LOGC + CRRN/CRRD*log(reserve()) 106 | // i.e. totalSupply = C * reserve()**CRR 107 | // reserve equals balance minus payouts 108 | // reserve() = this.balance - \sum_{addr:address} dividends(addr) 109 | 110 | function transferTokens( 111 | address _from, 112 | address _to, 113 | uint256 _value 114 | ) internal { 115 | if (balanceOfOld[_from] < _value) revert(); 116 | if (_to == address(this)) { 117 | sell(_value); 118 | } else { 119 | int256 payoutDiff = (int256)(earningsPerShare * _value); 120 | balanceOfOld[_from] -= _value; 121 | balanceOfOld[_to] += _value; 122 | payouts[_from] -= payoutDiff; 123 | payouts[_to] += payoutDiff; 124 | } 125 | Transfer(_from, _to, _value); 126 | } 127 | 128 | function transfer(address _to, uint256 _value) public { 129 | transferTokens(msg.sender, _to, _value); 130 | } 131 | 132 | function transferFrom( 133 | address _from, 134 | address _to, 135 | uint256 _value 136 | ) public { 137 | var _allowance = allowance[_from][msg.sender]; 138 | if (_allowance < _value) revert(); 139 | allowance[_from][msg.sender] = _allowance - _value; 140 | transferTokens(_from, _to, _value); 141 | } 142 | 143 | function approve(address _spender, uint256 _value) public { 144 | // To change the approve amount you first have to reduce the addresses` 145 | // allowance to zero by calling `approve(_spender, 0)` if it is not 146 | // already 0 to mitigate the race condition described here: 147 | // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 148 | if ((_value != 0) && (allowance[msg.sender][_spender] != 0)) revert(); 149 | allowance[msg.sender][_spender] = _value; 150 | Approval(msg.sender, _spender, _value); 151 | } 152 | 153 | function dividends(address _owner) 154 | public 155 | constant 156 | returns (uint256 amount) 157 | { 158 | return 159 | (uint256)( 160 | (int256)(earningsPerShare * balanceOfOld[_owner]) - 161 | payouts[_owner] 162 | ) / PRECISION; 163 | } 164 | 165 | function withdrawOld(address to) public { 166 | var balance = dividends(msg.sender); 167 | payouts[msg.sender] += (int256)(balance * PRECISION); 168 | totalPayouts += (int256)(balance * PRECISION); 169 | to.transfer(balance); 170 | } 171 | 172 | function balance() internal constant returns (uint256 amount) { 173 | return this.balance - msg.value; 174 | } 175 | 176 | function reserve() public constant returns (uint256 amount) { 177 | return 178 | balance() - 179 | ((uint256)( 180 | (int256)(earningsPerShare * totalSupply) - totalPayouts 181 | ) / PRECISION) - 182 | 1; 183 | } 184 | 185 | function buy() internal { 186 | if (msg.value < 0.000001 ether || msg.value > 1000000 ether) revert(); 187 | var sender = msg.sender; 188 | // 5 % of the amount is used to pay holders. 189 | var fee = (uint256)(msg.value / 10); 190 | 191 | // compute number of bought tokens 192 | var numEther = msg.value - fee; 193 | var numTokens = getTokensForEther(numEther); 194 | 195 | var buyerfee = fee * PRECISION; 196 | if (totalSupply > 0) { 197 | // compute how the fee distributed to previous holders and buyer. 198 | // The buyer already gets a part of the fee as if he would buy each token separately. 199 | var holderreward = ((PRECISION - 200 | ((reserve() + numEther) * numTokens * PRECISION) / 201 | (totalSupply + numTokens) / 202 | numEther) * (uint256)(CRRD)) / (uint256)(CRRD - CRRN); 203 | var holderfee = fee * holderreward; 204 | buyerfee -= holderfee; 205 | 206 | // Fee is distributed to all existing tokens before buying 207 | var feePerShare = holderfee / totalSupply; 208 | earningsPerShare += feePerShare; 209 | } 210 | // add numTokens to total supply 211 | totalSupply += numTokens; 212 | // add numTokens to balance 213 | balanceOfOld[sender] += numTokens; 214 | // fix payouts so that sender doesn't get old earnings for the new tokens. 215 | // also add its buyerfee 216 | var payoutDiff = (int256)((earningsPerShare * numTokens) - buyerfee); 217 | payouts[sender] += payoutDiff; 218 | totalPayouts += payoutDiff; 219 | } 220 | 221 | function sell(uint256 amount) internal { 222 | var numEthers = getEtherForTokens(amount); 223 | // remove tokens 224 | totalSupply -= amount; 225 | balanceOfOld[msg.sender] -= amount; 226 | 227 | // fix payouts and put the ethers in payout 228 | var payoutDiff = (int256)( 229 | earningsPerShare * amount + (numEthers * PRECISION) 230 | ); 231 | payouts[msg.sender] -= payoutDiff; 232 | totalPayouts -= payoutDiff; 233 | } 234 | 235 | function getTokensForEther(uint256 ethervalue) 236 | public 237 | constant 238 | returns (uint256 tokens) 239 | { 240 | return 241 | fixedExp((fixedLog(reserve() + ethervalue) * CRRN) / CRRD + LOGC) - 242 | totalSupply; 243 | } 244 | 245 | function getEtherForTokens(uint256 tokens) 246 | public 247 | constant 248 | returns (uint256 ethervalue) 249 | { 250 | if (tokens == totalSupply) return reserve(); 251 | return 252 | reserve() - 253 | fixedExp(((fixedLog(totalSupply - tokens) - LOGC) * CRRD) / CRRN); 254 | } 255 | 256 | int256 constant one = 0x10000000000000000; 257 | uint256 constant sqrt2 = 0x16a09e667f3bcc908; 258 | uint256 constant sqrtdot5 = 0x0b504f333f9de6484; 259 | int256 constant ln2 = 0x0b17217f7d1cf79ac; 260 | int256 constant ln2_64dot5 = 0x2cb53f09f05cc627c8; 261 | int256 constant c1 = 0x1ffffffffff9dac9b; 262 | int256 constant c3 = 0x0aaaaaaac16877908; 263 | int256 constant c5 = 0x0666664e5e9fa0c99; 264 | int256 constant c7 = 0x049254026a7630acf; 265 | int256 constant c9 = 0x038bd75ed37753d68; 266 | int256 constant c11 = 0x03284a0c14610924f; 267 | 268 | function fixedLog(uint256 a) internal pure returns (int256 log) { 269 | int32 scale = 0; 270 | while (a > sqrt2) { 271 | a /= 2; 272 | scale++; 273 | } 274 | while (a <= sqrtdot5) { 275 | a *= 2; 276 | scale--; 277 | } 278 | int256 s = (((int256)(a) - one) * one) / ((int256)(a) + one); 279 | // The polynomial R = c1*x + c3*x^3 + ... + c11 * x^11 280 | // approximates the function log(1+x)-log(1-x) 281 | // Hence R(s) = log((1+s)/(1-s)) = log(a) 282 | var z = (s * s) / one; 283 | return 284 | scale * 285 | ln2 + 286 | ((s * 287 | (c1 + 288 | ((z * 289 | (c3 + 290 | ((z * 291 | (c5 + 292 | ((z * 293 | (c7 + 294 | ((z * (c9 + ((z * c11) / one))) / 295 | one))) / one))) / one))) / 296 | one))) / one); 297 | } 298 | 299 | int256 constant c2 = 0x02aaaaaaaaa015db0; 300 | int256 constant c4 = -0x000b60b60808399d1; 301 | int256 constant c6 = 0x0000455956bccdd06; 302 | int256 constant c8 = -0x000001b893ad04b3a; 303 | 304 | function fixedExp(int256 a) internal pure returns (uint256 exp) { 305 | int256 scale = (a + (ln2_64dot5)) / ln2 - 64; 306 | a -= scale * ln2; 307 | // The polynomial R = 2 + c2*x^2 + c4*x^4 + ... 308 | // approximates the function x*(exp(x)+1)/(exp(x)-1) 309 | // Hence exp(x) = (R(x)+x)/(R(x)-x) 310 | int256 z = (a * a) / one; 311 | int256 R = ((int256)(2) * one) + 312 | ((z * 313 | (c2 + 314 | ((z * (c4 + ((z * (c6 + ((z * c8) / one))) / one))) / 315 | one))) / one); 316 | exp = (uint256)(((R + a) * one) / (R - a)); 317 | if (scale >= 0) exp <<= scale; 318 | else exp >>= -scale; 319 | return exp; 320 | } 321 | 322 | /*function destroy() external { 323 | selfdestruct(owner); 324 | }*/ 325 | 326 | function() public payable { 327 | if (msg.value > 0) buy(); 328 | else withdrawOld(msg.sender); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /ProofofWeakHandsCoin/img/approve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/ProofofWeakHandsCoin/img/approve.png -------------------------------------------------------------------------------- /ProofofWeakHandsCoin/img/transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/ProofofWeakHandsCoin/img/transfer.png -------------------------------------------------------------------------------- /ProofofWeakHandsCoin/img/transferFrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/ProofofWeakHandsCoin/img/transferFrom.png -------------------------------------------------------------------------------- /ProofofWeakHandsCoin/img/withdraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/ProofofWeakHandsCoin/img/withdraw.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summary Analysis of the major exploits that took place on Ethereum 2 | 3 | Each exploit has a more detailed analysis in its own folder, with the vulnerabilities of the contract(s), the addresses involved and the exploit itself with the transactions the hacker made to exploit the contract. 4 | 5 | - FTX Gas Abuse - 81 ETH "used" 71 ETH gained (2022-10-12) 6 | - TempleDAO - 1,830 ETH (2022-10-11) 7 | - Meebits - 200ETH (2021-05-08) 8 | - Proof of Weak Hands Coin - 866 ETH (2018-02-01) 9 | - Multi-sig Wallet 2, Parity Bug - >500,000 ETH lost (2017-11-06) 10 | - Multi-sig Wallet - 150,000 ETH (2017-07-19) 11 | 12 | --- 13 | 14 | ## FTX Exchange Gas Abuse 15 | In October 2022, a user abused free gas, offer by the FTX exchange for their withdraw, to mint XEN token, which requires no ETH, only gas for the transaction. It costed FTX over 81 ETH in gas fee and the exploiter made over 61 ETH by swapping the obtain XEN for ETH, WBTC and USDC. 16 | 17 | ## The vulnerability: Gas limit too high on withdraw 18 | 19 | The FTX exchange offer free withdraw (transaction sent from the FTX address), with a transaction where the gas limit is set to 500'000. The withdraw transaction call the `fallback()` function on the address where the funds are to be sent, if the address is a contract, it will then executed whatever code is in that function until the gas runs out. With a gas limit of 500'000, there is a lot of gas left after the ETH transfer. A user can then use that gas to do anything they want. 20 | 21 | ## The exploit 22 | 23 | A user decided to use that free gas to mint some XEN. XEN is an ERC-20 token where the mint function requires no ETH just the gas needed for the transaction. 24 | The exploiter deployed an exploitContract and made the FTX exchange send it some ETH. 25 | When the FTX exchange called the exploitContract `fallback()` function to sent some ETH (0.0035ETH), the exploitContract will use as much as the 500'000 gas limit to mint XEN tokens. It first deploys a dummy contract, mint the token with `XEN.claimRank(1)` with this contract and then selfdestruct. He finally send the received ETH to the exploiterAddr. Since some gas is left, he does it again with other dummy contracts. An address can only do one mint at the time, and has to wait at least a day until it can withdraw the token to mint again. He did dozens of similar transactions. All gas fee are paid by the FTX exchange. 26 | 27 | After a day, the tokens can be claimed, so the contracts will now call `XEN.claimMintRewardAndShare(exploiterAddr, 100)`, claiming their token and sharing them all with the exploiter address. It will then remint some token (and claim them a day later). Finally it selfdestruct the dummyContract. Since at this point there is still some unused gas, he does the same two more time with two other dummy contracts. At the end, the `fallback()` function send the received 0.0035ETH to the exploiter address. Again, since the FTX address is the sender of the transaction, so it pays all the gas fee. 28 | 29 | ![claimMintReward transaction](FTXGasAbuse/img/claimMintRewardtx.png "claimMintReward transaction") 30 | 31 | In one transaction, the exploiter ends up with around 171,000 XEN, the FTX address loses around 0.01 ETH in gas fee. 32 | 33 | --- 34 | 35 | ## TempleDAO - 1,830 ETH (2022-10-11) 36 | 37 | 38 | ### The vulnerability: Access control 39 | 40 | Poor access control on the `migrateStake()` function of StaxLPStaking: 41 | 42 | ```solidity 43 | /** 44 | * @notice For migrations to a new staking contract: 45 | * 1. User/DApp checks if the user has a balance in the `oldStakingContract` 46 | * 2. If yes, user calls this function `newStakingContract.migrateStake(oldStakingContract, balance)` 47 | * 3. Staking balances are migrated to the new contract, user will start to earn rewards in the new contract. 48 | * 4. Any claimable rewards in the old contract are sent directly to the user's wallet. 49 | * @param oldStaking The old staking contract funds are being migrated from. 50 | * @param amount The amount to migrate - generally this would be the staker's balance 51 | */ 52 | function migrateStake(address oldStaking, uint256 amount) external { 53 | StaxLPStaking(oldStaking).migrateWithdraw(msg.sender, amount); //exploit 54 | _applyStake(msg.sender, amount); 55 | } 56 | ``` 57 | 58 | This function allows **anyone** (missing modifier) to submit **any address** (missing check) as the `oldStaking` parameter. 59 | 60 | The first line call `migrateWithdraw` on the *supplied address*, anyone can choose what this functin does (eg nothing at all), and the second line increase the msg.sender's balance on the contract, since the contract thinks that the msg.sender had something staked that was just migrate: 61 | ```solidity 62 | function _applyStake(address _for, uint256 _amount) internal updateReward(_for) { 63 | _totalSupply += _amount; 64 | _balances[_for] += _amount; 65 | emit Staked(_for, _amount); 66 | } 67 | ``` 68 | 69 | Once this is done, we can just call `withdrawAll()` which will transfer the given amount of LP tokens to our msg.sender address. 70 | 71 | ![Exploit tx](TempleDAO/img/exploit_tx.png "Exploit tx") 72 | 73 | --------- 74 | 75 | 76 | ## Meebits - 200ETH (2021-05-08) 77 | 78 | In May 2021, an attacker, armed with the list of rare Meebits tokenIds, managed to brute force the early mint of the NFT and revert all transactions except the ones that would mint a rare NFT. He managed to mint one before the mint was paused, and sold it for 200 ETH. 79 | 80 | ### The vulnerability: leaked rarity traits 81 | 82 | Inside a NFT collection, some token are more rare than others, and thus more valuable. The rarity is baseed on the token's traits. Unfortunately, for the Meebits NFT, the contract contained a file with the metadata of the collection, containing the traits and their rarity for each tokenId. So when you minted an NFT, you could know its relative value. 83 | 84 | ```solidity 85 | // IPFS Hash to the NFT content 86 | string public contentHash = 87 | "QmfXYgfX1qNfzQ6NRyFnupniZusasFPMeiWn5aaDnx7YXo"; 88 | ``` 89 | 90 | Since the `mint()` function returns the id of the minted token, a contract could easily call the `mint()` function and `revert` if the returned id was not in a list of rare tokenId. 91 | 92 | ### The exploit 93 | 94 | The attacker deployed his attack contract, with a list of rare tokenId, and an `attack()` function which simply called `mintWithPunkOrGlyph()` and made sure the minted token was rare, otherwise the tx would revert. It also sent 1 ETH to the miner to ensure that the transaction would be added to the block: 95 | 96 | ```solidity 97 | function attack(uint _punkId) public{ 98 | uint256 tokenId = meebits.mintWithPunkOrGlyph(_punkId); // mint a meebits 99 | require(rareMeebits[tokenId]); //check if its a rare one, revert if not 100 | block.coinbase.call{value: 1 ether}(""); // pay the miner 101 | } 102 | ``` 103 | 104 | ![Successful mint transaction](Meebits/img/successful_brute_force_mint.png "Successful mint transaction") 105 | 106 | He then quickly sold Meebits #16647 for 200 ETH. 107 | 108 | --------- 109 | 110 | ## Proof of Weak Hands Coin - 866 ETH (2018-02-01) 111 | 112 | 4chan decided to create a crypto ponzi scheme, which was advertised as such, and obviously it worked well. In only three days it had over 1,000 ETH, but an underflow vulnerability allowed someone to withdraw 866 ETH from the ponzi. 113 | 114 | ### The vulnerability: Underflow 115 | 116 | There is two issues in this contract: an underflow vulnerability and a poor internal logic that can potentialy lead to this underflow. 117 | The underflow vulnerability of the contract is in the `sell()` function: 118 | ```solidity 119 | function sell(uint256 amount) internal { 120 | var numEthers = getEtherForTokens(amount); 121 | // remove tokens 122 | totalSupply -= amount; 123 | balanceOfOld[msg.sender] -= amount; //** possible underflow if amount is bigger that the balance 124 | 125 | // fix payouts and put the ethers in payout 126 | var payoutDiff = (int256)( 127 | earningsPerShare * amount + (numEthers * PRECISION) 128 | ); 129 | payouts[msg.sender] -= payoutDiff; 130 | totalPayouts -= payoutDiff; 131 | } 132 | ``` 133 | `sell()` is called when a user call `tranfer()` with `_to` equal to the contract address. There is in fact a check to ensure that when a user wants to sell his tokens, he has that amount of token. But since `transferTokens(_from, _to, _value)` allows a user to transfer tokens from a different address (who has previously approved the user to do so), the check is done for that `_from` address and if that address indeed has those tokens, the check pass. But when `sell()` decrease the balance, it does so to the `msg.sender` address instead of the `_from` so if the user (`msg.sender`) has less tokens that the amount passed in `_value`, an underflow will occur. The error is that `_from` is not passed to `sell()` as an argument. 134 | 135 | 136 | ### The exploit 137 | 138 | The attacker used an underflow to increase his token balance to the maximum amount and then used that balance to exit the Ponzi with a lot of ETH. 139 | 140 | He first used a second account (0x945) to buy some tokens and to approv his first account (0xb9cd) to transfer some of his tokens. Now that he can transfer tokens on behalf of his other account, he called `transferFrom(_from_=0x945, _to=PonziContract, _value=1)`. `transferToken()` check that the balance of `_from` is high enough to transfer `_value` of tokens (it is) and since `_to` is the address of the contract, call `sell()`. But since `sell()` doesn't receive the `_from` parameter (0x945), it assumes that it is the `msg.sender` (0xb9cd), which doesn't have any token, so `balanceOfOld[msg.sender] -= amount` underflow and is now equal to the maximum amount of `uint256`. 141 | 142 | He then sold some of his tokens and called `withdraw()` which allowed him to exit the Ponzi with over 866 ETH: 143 | 144 | ![withdraw transaction](ProofofWeakHandsCoin/img/withdraw.png "withdraw transaction") 145 | 146 | --- 147 | 148 | ## Multi-sig Wallet 2, Parity Bug - >500,000 ETH lost (2017-11-06) 149 | 150 | After the MultiSig attack of July 2017 (where anyone could reinitialize any multisig wallet thanks to `initWallet()`, become owner, and withdraw all funds), the Parity team deployed their new Parity wallet library with the previous vulnerability "fixed". 151 | A few month later, a user "accidentally" managed to suicide the library contract by calling the very same `initWallet()` function, since there had been no initialization on the library contract. 152 | 153 | ### The vulnerability: Uninitialization 154 | 155 | The main vulnerability again involve `initWallet()`, only now it can only be called once. The function was correctly written, from the point of view of the Wallet contract, to make it uncallable after been initialized once. But since it was never called on the library contract itself, any one could call it and become owner of the **library**. Becoming owner of the library is not very interesting, you can change the library storage a bit but that is all. 156 | The big problem comes from the fact that the contract was a library, which was used by a lot (500+) of different Parity Wallet smart contract. If this library were to disappear (become uncallable) those contracts who rely on it for their logic would simply become unusable. 157 | The library have a `kill()` function that call `selfdestruct()` (`suicide()`). It is protected by ownership, but since `initWallet()` can give ownership to anyone, the first user to call `initWallet()` can now destroy the library and all attached contracts. 158 | 159 | When the library gets killed, all contracts whose functions `delegatecall()` to it will no longer work. Since, in the Parity Wallet, the library address is a constant variable and thus hardcoded in the bytecode of the contract once deploy: 160 | ```solidity 161 | address constant _walletLibrary = 0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4; 162 | ``` 163 | those contracts become completely unusable. 164 | 165 | ### The exploit 166 | 167 | The attacker first called `initWallet([attacker_addr],0,0)` to become owner of the library contract and then simply destroy it with `kill()`. He then opened an issue on Github: 168 | ![Issue on GitHub](ParityBug/img/github.png "Issue on GitHub") 169 | 170 | ---- 171 | 172 | ## Multi-sig Wallet - 150,000 ETH (2017-07-19) 173 | 174 | 175 | The multi sig wallet works with a library and use `delegatecall`. If someone calls `initWallet()` on the Wallet, it will delegate the call to the Library which will execute **within the context** of the Wallet contract: 176 | ```solidity 177 | function initWallet( 178 | address[] _owners, 179 | uint256 _required, 180 | uint256 _daylimit 181 | ) { 182 | initMultiowned(_owners, _required); //sett owner and n. of required sig 183 | initDaylimit(_daylimit); // set daily limit for withdraw 184 | } 185 | ``` 186 | With `initMultiowned()`: 187 | ```solidity 188 | // constructor is given number of sigs required to do protected "onlymanyowners" transactions 189 | // as well as the selection of addresses capable of confirming them. 190 | // change from original: msg.sender is not automatically owner 191 | function initMultiowned(address[] _owners, uint256 _required) { 192 | m_numOwners = _owners.length; //1 193 | m_required = _required; //0 194 | 195 | for (uint256 i = 0; i < _owners.length; ++i) { 196 | m_owners[1 + i] = uint256(_owners[i]); //attacker becomes owner 197 | m_ownerIndex[uint256(_owners[i])] = 1 + i; 198 | } 199 | } 200 | ``` 201 | So by calling `initWallet([attacker_addr], 0,_)`, the attacker becomes owner of the Wallet, change the number of signature required to 0 and the daily limit of widrawnable funds to the balance of the wallet. He can then withdraw the funds by calling `execute(attacker_addr, amount, _)`: 202 | ```solidity 203 | // Outside-visible transact entry point. Executes transaction immediately if below daily spend limit. 204 | // If not, goes into multisig process. We provide a hash on return to allow the sender to provide 205 | // shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value 206 | // and _data arguments). They still get the option of using them if they want, anyways. 207 | function execute( 208 | address _to, 209 | uint256 _value, 210 | bytes _data 211 | ) onlyowner returns (bool _callValue) { 212 | // first, take the opportunity to check that we're under the daily limit. 213 | if (underLimit(_value)) { 214 | SingleTransact(msg.sender, _value, _to, _data); 215 | // yes - just execute the call. 216 | _callValue = _to.call.value(_value)(_data); // transfer funds to attacker 217 | } else {...} 218 | } 219 | ``` 220 | 221 | 222 | ![Init transaction](MultiSig/img/init_tx.png "Init transaction") 223 | -------------------------------------------------------------------------------- /TempleDAO/Analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis of the TempleDAO exploit 2 | 3 | The exploited StaxLPStaking contract on etherscan: [0xd2869042E12a3506100af1D192b5b04D65137941](https://etherscan.io/address/0xd2869042e12a3506100af1d192b5b04d65137941#code) 4 | The exploit transaction hash: [0x8c3f442fc6d640a6ff3ea0b12be64f1d4609ea94edd2966f42c01cd9bdcf04b5](https://etherscan.io/tx/0x8c3f442fc6d640a6ff3ea0b12be64f1d4609ea94edd2966f42c01cd9bdcf04b5) 5 | The transaction hash with the swaps: [0x4b119a4f4ba1ad483e9851973719f310527b43f3fcc827b6d52db9f4c1ddb6a2](https://etherscan.io/tx/0x4b119a4f4ba1ad483e9851973719f310527b43f3fcc827b6d52db9f4c1ddb6a2) 6 | 7 | The hacker address: [0x9c9Fb3100A2a521985F0c47DE3B4598dafD25B01](https://etherscan.io/address/0x9c9fb3100a2a521985f0c47de3b4598dafd25b01) 8 | The Exploit contract: [0x2Df9c154fe24D081cfE568645Fb4075d725431e0](https://etherscan.io/address/0x2df9c154fe24d081cfe568645fb4075d725431e0) 9 | Hacker exit address: [0x2B63d4A3b2DB8AcBb2671ea7B16993077F1DB5A0](https://etherscan.io/address/0x2b63d4a3b2db8acbb2671ea7b16993077f1db5a0) 10 | 11 | ## The vulnerability: Access control 12 | 13 | Poor access control on the `migrateStake()` function of StaxLPStaking: 14 | 15 | ```solidity 16 | /** 17 | * @notice For migrations to a new staking contract: 18 | * 1. User/DApp checks if the user has a balance in the `oldStakingContract` 19 | * 2. If yes, user calls this function `newStakingContract.migrateStake(oldStakingContract, balance)` 20 | * 3. Staking balances are migrated to the new contract, user will start to earn rewards in the new contract. 21 | * 4. Any claimable rewards in the old contract are sent directly to the user's wallet. 22 | * @param oldStaking The old staking contract funds are being migrated from. 23 | * @param amount The amount to migrate - generally this would be the staker's balance 24 | */ 25 | function migrateStake(address oldStaking, uint256 amount) external { 26 | StaxLPStaking(oldStaking).migrateWithdraw(msg.sender, amount); //exploit 27 | _applyStake(msg.sender, amount); 28 | } 29 | ``` 30 | 31 | This function allows **anyone** (missing modifier) to submit **any address** (missing check) as the `oldStaking` parameter. 32 | 33 | The first line call `migrateWithdraw` on the *supplied address*, anyone can choose what this functin does (eg nothing at all), and the second line increase the msg.sender's balance on the contract, since the contract thinks that the msg.sender had something staked that was just migrate: 34 | ```solidity 35 | function _applyStake(address _for, uint256 _amount) internal updateReward(_for) { 36 | _totalSupply += _amount; 37 | _balances[_for] += _amount; 38 | emit Staked(_for, _amount); 39 | } 40 | ``` 41 | 42 | Once this is done, we can just call `withdrawAll()` which will transfer the given amount of LP tokens to our msg.sender address. 43 | 44 | 45 | ## The exploit 46 | 47 | The hacker did exactly that, he deployed a dummy oldStaking contract and called `migrateStake(dummy_oldStking, xFraxTempleLP.balanceOf(StaxLPStaking))` and then `withdrawAll(false)`. He submitted his transaction to the flashbot rpc to avoid frontrunners, and ended up with 321,154 [xFraxTempleLP](0xBcB8b7FC9197fEDa75C101fA69d3211b5a30dCD9) worth over $2MM. 48 | 49 | Overall, this was a fairly simple exploit. 50 | 51 | ![Exploit tx](img/exploit_tx.png "Exploit tx") 52 | 53 | Once the hacker receive the 321,154 LP token, he does several swap to end up with 1,830 ETH worth around $2,350,000 which he sent to tornado cash. 54 | 55 | ![Swaps](img/swaps.png "Swaps") 56 | 57 | 58 | The hacker through a first address: 59 | - funded its EOA through finance (1) 60 | - deployed a dummy contract at 0x9bdb04493aF17eB318A23BfeFe43f07b3E58EcFb (2) (later used as the address for `oldStaking`) 61 | - deployed the attack contract at 0x2Df9c154fe24D081cfE568645Fb4075d725431e0 (3) 62 | 63 | ![contract deployment txs](img/contract_deployment.png " contract deployment txs") 64 | 65 | 66 | And a second address: 67 | - exploited the stacking contract (4) 68 | - swaped his newly gain LP token for ETH (5) 69 | - sent the ETH to a third exit address 0x2B63d4A3b2DB8AcBb2671ea7B16993077F1DB5A0 (6) 70 | 71 | ![hacker txs](img/hacker_tx_summary.png " hacker txs") 72 | 73 | 74 | - which sent 1,813 ETH to tornado cash (7) 75 | 76 | ![tornado txs](img/tornado.png " tornado txs") 77 | 78 | 79 | ## The Aftermath 80 | 81 | Since the exploit, the Stax.fi website has been taken down: 82 | 83 | ![Stax.fi](img/stax.fi.png "Stax.fi") 84 | -------------------------------------------------------------------------------- /TempleDAO/StaxLPStaking.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.4; 2 | // SPDX-License-Identifier: AGPL-3.0-or-later 3 | 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 9 | 10 | 11 | /** 12 | * Based on synthetix BaseRewardPool.sol & convex cvxLocker 13 | * Modified for use by TempleDAO 14 | */ 15 | 16 | contract StaxLPStaking is Ownable { 17 | 18 | using SafeERC20 for IERC20; 19 | 20 | IERC20 public stakingToken; 21 | address public rewardDistributor; 22 | 23 | uint256 public constant DURATION = 86400 * 7; 24 | uint256 private _totalSupply; 25 | 26 | address[] public rewardTokens; 27 | 28 | mapping(address => uint256) private _balances; 29 | mapping(address => Reward) public rewardData; 30 | mapping(address => mapping(address => uint256)) public claimableRewards; 31 | mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid; 32 | 33 | /// @dev For use when migrating to a new staking contract. 34 | address public migrator; 35 | 36 | struct Reward { 37 | uint40 periodFinish; 38 | uint216 rewardRate; // The reward amount (1e18) per total reward duration 39 | uint40 lastUpdateTime; 40 | uint216 rewardPerTokenStored; 41 | } 42 | 43 | event RewardAdded(address token, uint256 amount); 44 | event Staked(address indexed user, uint256 amount); 45 | event Withdrawn(address indexed user, address toAddress, uint256 amount); 46 | event RewardPaid(address indexed user, address toAddress, address rewardToken, uint256 reward); 47 | event UpdatedRewardDistributor(address distributor); 48 | event MigratorSet(address migrator); 49 | 50 | constructor(address _stakingToken, address _distributor) { 51 | stakingToken = IERC20(_stakingToken); 52 | rewardDistributor = _distributor; 53 | } 54 | 55 | // set distributor of rewards 56 | function setRewardDistributor(address _distributor) external onlyOwner { 57 | rewardDistributor = _distributor; 58 | 59 | emit UpdatedRewardDistributor(_distributor); 60 | } 61 | 62 | function totalSupply() public view returns (uint256) { 63 | return _totalSupply; 64 | } 65 | 66 | function balanceOf(address account) public view returns (uint256) { 67 | return _balances[account]; 68 | } 69 | 70 | function addReward(address _rewardToken) external onlyOwner { 71 | require(rewardData[_rewardToken].lastUpdateTime == 0, "exists"); 72 | rewardTokens.push(_rewardToken); 73 | rewardData[_rewardToken].lastUpdateTime = uint40(block.timestamp); 74 | rewardData[_rewardToken].periodFinish = uint40(block.timestamp); 75 | } 76 | 77 | function _rewardPerToken(address _rewardsToken) internal view returns (uint256) { 78 | if (totalSupply() == 0) { 79 | return rewardData[_rewardsToken].rewardPerTokenStored; 80 | } 81 | 82 | return 83 | rewardData[_rewardsToken].rewardPerTokenStored + 84 | (((_lastTimeRewardApplicable(rewardData[_rewardsToken].periodFinish) - 85 | rewardData[_rewardsToken].lastUpdateTime) * 86 | rewardData[_rewardsToken].rewardRate * 1e18) 87 | / totalSupply()); 88 | } 89 | 90 | function rewardPerToken(address _rewardsToken) external view returns (uint256) { 91 | return _rewardPerToken(_rewardsToken); 92 | } 93 | 94 | function rewardPeriodFinish(address _token) external view returns (uint40) { 95 | return rewardData[_token].periodFinish; 96 | } 97 | 98 | function earned(address _account, address _rewardsToken) external view returns (uint256) { 99 | return _earned(_account, _rewardsToken, _balances[_account]); 100 | } 101 | 102 | function _earned( 103 | address _account, 104 | address _rewardsToken, 105 | uint256 _balance 106 | ) internal view returns (uint256) { 107 | return 108 | (_balance * (_rewardPerToken(_rewardsToken) - userRewardPerTokenPaid[_account][_rewardsToken])) / 1e18 + 109 | claimableRewards[_account][_rewardsToken]; 110 | } 111 | 112 | function stake(uint256 _amount) external { 113 | stakeFor(msg.sender, _amount); 114 | } 115 | 116 | function stakeAll() external { 117 | uint256 balance = stakingToken.balanceOf(msg.sender); 118 | stakeFor(msg.sender, balance); 119 | } 120 | 121 | function stakeFor(address _for, uint256 _amount) public { 122 | require(_amount > 0, "Cannot stake 0"); 123 | 124 | // pull tokens and apply stake 125 | stakingToken.safeTransferFrom(msg.sender, address(this), _amount); 126 | _applyStake(_for, _amount); 127 | } 128 | 129 | function _applyStake(address _for, uint256 _amount) internal updateReward(_for) { 130 | _totalSupply += _amount; 131 | _balances[_for] += _amount; 132 | emit Staked(_for, _amount); 133 | } 134 | 135 | function _withdrawFor( 136 | address staker, 137 | address toAddress, 138 | uint256 amount, 139 | bool claimRewards, 140 | address rewardsToAddress 141 | ) internal updateReward(staker) { 142 | require(amount > 0, "Cannot withdraw 0"); 143 | require(_balances[staker] >= amount, "Not enough staked tokens"); 144 | 145 | _totalSupply -= amount; 146 | _balances[staker] -= amount; 147 | 148 | stakingToken.safeTransfer(toAddress, amount); 149 | emit Withdrawn(staker, toAddress, amount); 150 | 151 | if (claimRewards) { 152 | // can call internal because user reward already updated 153 | _getRewards(staker, rewardsToAddress); 154 | } 155 | } 156 | 157 | function withdraw(uint256 amount, bool claim) public { 158 | _withdrawFor(msg.sender, msg.sender, amount, claim, msg.sender); 159 | } 160 | 161 | function withdrawAll(bool claim) external { 162 | _withdrawFor(msg.sender, msg.sender, _balances[msg.sender], claim, msg.sender); 163 | } 164 | 165 | function getRewards(address staker) external updateReward(staker) { 166 | _getRewards(staker, staker); 167 | } 168 | 169 | // @dev internal function. make sure to call only after updateReward(account) 170 | function _getRewards(address staker, address rewardsToAddress) internal { 171 | for (uint256 i; i < rewardTokens.length; i++) { 172 | _getReward(staker, rewardTokens[i], rewardsToAddress); 173 | } 174 | } 175 | 176 | function getReward(address staker, address rewardToken) external updateReward(staker) { 177 | _getReward(staker, rewardToken, staker); 178 | } 179 | 180 | function _getReward(address staker, address rewardToken, address rewardsToAddress) internal { 181 | uint256 amount = claimableRewards[staker][rewardToken]; 182 | if (amount > 0) { 183 | claimableRewards[staker][rewardToken] = 0; 184 | IERC20(rewardToken).safeTransfer(rewardsToAddress, amount); 185 | 186 | emit RewardPaid(staker, rewardsToAddress, rewardToken, amount); 187 | } 188 | } 189 | 190 | function _lastTimeRewardApplicable(uint256 _finishTime) internal view returns (uint256) { 191 | if (_finishTime < block.timestamp) { 192 | return _finishTime; 193 | } 194 | return block.timestamp; 195 | } 196 | 197 | function _notifyReward(address _rewardsToken, uint256 _amount) internal { 198 | Reward storage rdata = rewardData[_rewardsToken]; 199 | 200 | if (block.timestamp >= rdata.periodFinish) { 201 | rdata.rewardRate = uint216(_amount / DURATION); 202 | } else { 203 | uint256 remaining = uint256(rdata.periodFinish) - block.timestamp; 204 | uint256 leftover = remaining * rdata.rewardRate; 205 | rdata.rewardRate = uint216((_amount + leftover) / DURATION); 206 | } 207 | 208 | rdata.lastUpdateTime = uint40(block.timestamp); 209 | rdata.periodFinish = uint40(block.timestamp + DURATION); 210 | } 211 | 212 | function notifyRewardAmount( 213 | address _rewardsToken, 214 | uint256 _amount 215 | ) external updateReward(address(0)) { 216 | require(msg.sender == rewardDistributor, "not distributor"); 217 | require(_amount > 0, "No reward"); 218 | require(rewardData[_rewardsToken].lastUpdateTime != 0, "unknown reward token"); 219 | 220 | _notifyReward(_rewardsToken, _amount); 221 | 222 | IERC20(_rewardsToken).safeTransferFrom(msg.sender, address(this), _amount); 223 | 224 | emit RewardAdded(_rewardsToken, _amount); 225 | } 226 | 227 | function setMigrator(address _migrator) external onlyOwner { 228 | migrator = _migrator; 229 | emit MigratorSet(_migrator); 230 | } 231 | 232 | /** 233 | * @notice For migrations to a new staking contract: 234 | * 1. User/DApp checks if the user has a balance in the `oldStakingContract` 235 | * 2. If yes, user calls this function `newStakingContract.migrateStake(oldStakingContract, balance)` 236 | * 3. Staking balances are migrated to the new contract, user will start to earn rewards in the new contract. 237 | * 4. Any claimable rewards in the old contract are sent directly to the user's wallet. 238 | * @param oldStaking The old staking contract funds are being migrated from. 239 | * @param amount The amount to migrate - generally this would be the staker's balance 240 | */ 241 | function migrateStake(address oldStaking, uint256 amount) external { 242 | StaxLPStaking(oldStaking).migrateWithdraw(msg.sender, amount); 243 | _applyStake(msg.sender, amount); 244 | } 245 | 246 | /** 247 | * @notice For migrations to a new staking contract. 248 | * 1. Withdraw `staker`s tokens to the new staking contract (the migrator) 249 | * 2. Any existing rewards are claimed and sent directly to the `staker` 250 | * @dev Called only from the new staking contract (the migrator). 251 | * `setMigrator(new_staking_contract)` needs to be called first 252 | * @param staker The staker who is being migrated to a new staking contract. 253 | * @param amount The amount to migrate - generally this would be the staker's balance 254 | */ 255 | function migrateWithdraw(address staker, uint256 amount) external onlyMigrator { 256 | _withdrawFor(staker, msg.sender, amount, true, staker); 257 | } 258 | 259 | modifier onlyMigrator() { 260 | require(msg.sender == migrator, "not migrator"); 261 | _; 262 | } 263 | 264 | modifier updateReward(address _account) { 265 | { 266 | // stack too deep 267 | for (uint256 i = 0; i < rewardTokens.length; i++) { 268 | address token = rewardTokens[i]; 269 | rewardData[token].rewardPerTokenStored = uint216(_rewardPerToken(token)); 270 | rewardData[token].lastUpdateTime = uint40(_lastTimeRewardApplicable(rewardData[token].periodFinish)); 271 | if (_account != address(0)) { 272 | claimableRewards[_account][token] = _earned(_account, token, _balances[_account]); 273 | userRewardPerTokenPaid[_account][token] = uint256(rewardData[token].rewardPerTokenStored); 274 | } 275 | } 276 | } 277 | _; 278 | } 279 | } -------------------------------------------------------------------------------- /TempleDAO/img/contract_deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/TempleDAO/img/contract_deployment.png -------------------------------------------------------------------------------- /TempleDAO/img/exploit_tx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/TempleDAO/img/exploit_tx.png -------------------------------------------------------------------------------- /TempleDAO/img/hacker_tx_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/TempleDAO/img/hacker_tx_summary.png -------------------------------------------------------------------------------- /TempleDAO/img/stax.fi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/TempleDAO/img/stax.fi.png -------------------------------------------------------------------------------- /TempleDAO/img/swaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/TempleDAO/img/swaps.png -------------------------------------------------------------------------------- /TempleDAO/img/tornado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeSpa/ethereum-exploit/9789764d700e659aeb9e852b0dcf426f88063908/TempleDAO/img/tornado.png --------------------------------------------------------------------------------