I participated in flashbot mev-share ctf, my first time to study mev-share.
I solved 9 challenges except MevMagicNumberV3.
Actually, it was difficult for me to write a solve code using typescript and rust, so I decided to use python familar to me.
https://github.com/flashbots/mev-share-client-ts
https://github.com/paradigmxyz/mev-share-rs
Most of the participants seem to use the above libraries.
My solve sciprts are
https://github.com/kangsangsoo/CTF-Writeups/tree/main/mev-share-ctf
mev-share
https://docs.flashbots.net/flashbots-mev-share/overview
I don't know enough to explain mev-share in detail yet, so please refer to the link above.
MevShareCTFSimple
contract code
contract MevShareCTFSimple is MevShareCTFBase {
uint256 public activeBlock;
uint256 immutable captureId;
event Activate();
constructor(MevShareCaptureLogger _mevShareCaptureLogger, uint256 _captureId) MevShareCTFBase(_mevShareCaptureLogger) payable {
captureId = _captureId;
}
function activateRewardSimple() external payable onlyOwner {
activeBlock = block.number;
emit Activate();
}
function claimReward() external {
require (activeBlock == block.number);
activeBlock = 0;
mevShareCaptureLogger.registerCapture(captureId, tx.origin);
}
}
V1
https://goerli.etherscan.io/address/0x98997b55Bb271e254BEC8B85763480719DaB0E53#code
data from SSE
{
"hash": "0x3826165395e100a7c20bce858ffc22e590d36451e11ca496d8b95c9bf6c493eb",
"logs": [
{
"address": "0x98997b55bb271e254bec8b85763480719dab0e53",
"topics": [
"0x59d3ce47d6ad6c6003cef97d136155b29d88653eb355c8bed6e03fbf694570ca"
],
"data": "0x"
}
],
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x7530"
}
If logs address == contract address, then send the following bundle.
bundle = {
hash,
tx of calling claimReward(),
}
V2
https://goerli.etherscan.io/address/0x65459dD36b03Af9635c06BAD1930DB660b968278#code
V1 and V2 are a little different.
{
"hash": "0xa4d03b2243c447ee5249f5005dc67ca068c4223de38124e4311df16cc5d8c6fc",
"logs": null,
"txs": [
{
"to": "0x65459dd36b03af9635c06bad1930db660b968278",
"functionSelector": "0xa3c356e4"
}
],
"mevGasPrice": "0x2faf080",
"gasUsed": "0x7530"
}
If txs to == contract address, then send the following bundle.
bundle = {
hash,
tx of calling claimReward(),
}
V3
https://goerli.etherscan.io/address/0x1cdDB0BA9265bb3098982238637C2872b7D12474#code
It is equal to V2.
{
"hash": "0x062e018cc7685ef29876fdfca8559f4815d372987adb61d48ee0e33a0594aa61",
"logs": null,
"txs": [
{
"to": "0x1cddb0ba9265bb3098982238637c2872b7d12474",
"functionSelector": "0xa3c356e4",
"callData": "0xa3c356e4"
}
],
"mevGasPrice": "0x2faf080",
"gasUsed": "0x7530"
}
V4
https://goerli.etherscan.io/address/0x20a1A5857fDff817aa1BD8097027a841D4969AA5#code
We only know the hash from SSE.
We can put our transaction behind any transaction and wait until it succeeds.
If a block has a transaction activeBlock() call ahead of the transaction I put in it, it succeeds.
{
"hash": "0x8e586a5ec514d85e3b8d5337e89c8459b9899a70d370fdf96d0f8e226b7d04e4",
"logs": null,
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x7530"
}
bundle = {
any hash,
tx of calling claimReward(),
}
MevShareCTFTriple
https://goerli.etherscan.io/address/0x1eA6Fb65BAb1f405f8Bdb26D163e6984B9108478#code
contract MevShareCTFTriple is MevShareCTFBase {
uint256 public activeBlock;
mapping (address => mapping (uint256 => uint256)) addressBlockCount;
event Activate();
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFBase(_mevShareCaptureLogger) payable {
}
function activateRewardTriple() external payable onlyOwner {
activeBlock = block.number;
emit Activate();
}
function claimReward() external {
require (activeBlock == block.number);
require (tx.origin == msg.sender);
uint256 claimCount = addressBlockCount[tx.origin][block.number] + 1;
if (claimCount == 3) {
mevShareCaptureLogger.registerCapture(401, tx.origin);
return;
}
addressBlockCount[tx.origin][block.number] = claimCount;
}
}
{
"hash": "0x0f016bf66c03ff8f017b84b2a59d1cd915b1456d768d341d2832bb8f5ca7d1cb",
"logs": [
{
"address": "0x1ea6fb65bab1f405f8bdb26d163e6984b9108478",
"topics": [
"0x59d3ce47d6ad6c6003cef97d136155b29d88653eb355c8bed6e03fbf694570ca"
],
"data": "0x"
}
],
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x7530"
}
You have to call claimReward() 3 times in the same block.
Note that the nonce should increases
bundle = {
hash,
tx of calling claimReward(),
tx of calling claimReward(),
tx of calling claimReward(),
}
MevShareCTFNewContracts
https://goerli.etherscan.io/address/0x5eA0feA0164E5AA58f407dEBb344876b5ee10DEA#code
contract MevShareCTFNewContracts is MevShareCTFBase {
uint256 public magicNumber;
// maps addresses to child contracts, acts both as check for valid caller and which CTF is being targeted
// value of 1 = emitted by address
// value of 2 = emitted by salt
mapping (address => uint256) childContracts;
event Activate(address newlyDeployedContract);
event ActivateBySalt(bytes32 salt);
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFBase(_mevShareCaptureLogger) payable {
}
function proxyRegisterCapture() external {
uint256 childContractType = childContracts[msg.sender];
if (childContractType == 0) {
revert("Not called by a child contract");
}
mevShareCaptureLogger.registerCapture(300 + childContractType, tx.origin);
}
function activateRewardNewContract(bytes32 salt) external payable onlyOwner {
MevShareCTFNewContract newlyDroppedContract = new MevShareCTFNewContract{salt: salt}();
childContracts[address(newlyDroppedContract)] = 1;
emit Activate(address(newlyDroppedContract));
}
function activateRewardBySalt(bytes32 salt) external payable onlyOwner {
MevShareCTFNewContract newlyDroppedContract = new MevShareCTFNewContract{salt: salt}();
childContracts[address(newlyDroppedContract)] = 2;
emit ActivateBySalt(salt);
}
}
contract MevShareCTFNewContract {
MevShareCTFNewContracts immutable mevShareCTFNewContracts;
uint256 public activeBlock;
constructor() payable {
mevShareCTFNewContracts = MevShareCTFNewContracts(msg.sender);
activeBlock = block.number;
}
function claimReward() external {
require (activeBlock == block.number);
activeBlock = 0;
mevShareCTFNewContracts.proxyRegisterCapture();
}
}
MevShareCTFNewContracts makes a new contract with salt using create2.
We can calcaulate that address before it is made because it is deterministic address.
V1
logs include the new contract address. So we can just call claimReward().
{
"hash": "0x8f4daf824ce07ef5188247addae10c603634e3e2a81739f9c56be65ddecabec3",
"logs": [
{
"address": "0x5ea0fea0164e5aa58f407debb344876b5ee10dea",
"topics": [
"0xf7e9fe69e1d05372bc855b295bc4c34a1a0a5882164dd2b26df30a26c1c8ba15"
],
"data": "0x000000000000000000000000e4e76109da7b05c28f7df8c0116ad9cf75beeafb"
}
],
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x27100"
}
V2
logs data includes the salt used to make the new one so that we can calculate the new address.
{
"hash": "0x703834fb1f5826597e522a0eefd726bdf47b16119a10fcb53331da88b7c618ee",
"logs": [
{
"address": "0x5ea0fea0164e5aa58f407debb344876b5ee10dea",
"topics": [
"0x71fd33d3d871c60dc3d6ecf7c8e5bb086aeb6491528cce181c289a411582ff1c"
],
"data": "0xbdbaf29470fe0ba323d11900b4f327ba382dfdf8e7cd4f3cd87349c0905b81d0"
}
],
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x27100"
}
MevShareCTFMagicNumber
V1
https://goerli.etherscan.io/address/0x118Bcb654d9A7006437895B51b5cD4946bF6CdC2#code
contract MevShareCTFMagicNumberV1 is MevShareCTFMagicNumber {
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFMagicNumber(_mevShareCaptureLogger) payable {
}
function claimReward(uint256 _magicNumber) external {
require(claimRewardInternal(_magicNumber, 201));
}
}
contract MevShareCTFMagicNumber is MevShareCTFBase {
uint256 public activeBlock;
uint256 private magicNumber;
event Activate(uint256 lowerBound, uint256 upperBound);
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFBase(_mevShareCaptureLogger) payable {
}
function activateRewardMagicNumber(uint256 _lowerBound, uint256 _upperBound, uint256 _magicNumber) external payable onlyOwner {
require (_lowerBound <= _magicNumber && _upperBound >= _magicNumber);
activeBlock = block.number;
magicNumber = _magicNumber;
emit Activate(_lowerBound, _upperBound);
}
function claimRewardInternal(uint256 _magicNumber, uint256 _captureId) internal returns (bool) {
if (activeBlock != block.number || _magicNumber != magicNumber) {
return false;
}
activeBlock = 0;
magicNumber = 0;
mevShareCaptureLogger.registerCapture(_captureId, tx.origin);
return true;
}
}
{
"hash": "0xd83e6a0196e0916e6bff1cd70815c77a91c62be49bd181ff6a6c49d5d095d57b",
"logs": [
{
"address": "0x118bcb654d9a7006437895b51b5cd4946bf6cdc2",
"topics": [
"0x86a27c2047f889fafe51029e28e24f466422abe8a82c0c27de4683dda79a0b5d"
],
"data": "0x000000000000000000000000000000000000000000000000000d019ab51ef141000000000000000000000000000000000000000000000000000d019ab51ef169"
}
],
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x8ca0"
}
We only know lower bound and upper bound, so we can bruteforce with "canRevert": True
Acutally, during the ctf, I misunderstood that if I pass a wrong magicNumber, my transaction is revert.
But when I writing this post, canRevert didn't matter.
bundle = {
hash,
tx of calling claimReward() and canRevert: True,
tx of calling claimReward() and canRevert: True,
tx of calling claimReward() and canRevert: True,
}
V2
https://goerli.etherscan.io/address/0x9BE957D1c1c1F86Ba9A2e1215e9d9EEFdE615a56#code
contract MevShareCTFMagicNumberV2 is MevShareCTFMagicNumber {
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFMagicNumber(_mevShareCaptureLogger) payable {
}
function claimReward(uint256 _magicNumber) external {
require(tx.origin == msg.sender);
require(claimRewardInternal(_magicNumber, 202));
}
}
contract MevShareCTFMagicNumber is MevShareCTFBase {
uint256 public activeBlock;
uint256 private magicNumber;
event Activate(uint256 lowerBound, uint256 upperBound);
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFBase(_mevShareCaptureLogger) payable {
}
function activateRewardMagicNumber(uint256 _lowerBound, uint256 _upperBound, uint256 _magicNumber) external payable onlyOwner {
require (_lowerBound <= _magicNumber && _upperBound >= _magicNumber);
activeBlock = block.number;
magicNumber = _magicNumber;
emit Activate(_lowerBound, _upperBound);
}
function claimRewardInternal(uint256 _magicNumber, uint256 _captureId) internal returns (bool) {
if (activeBlock != block.number || _magicNumber != magicNumber) {
return false;
}
activeBlock = 0;
magicNumber = 0;
mevShareCaptureLogger.registerCapture(_captureId, tx.origin);
return true;
}
}
{
"hash": "0x3217b8c344748db113616ba72f3058632f08c468f5df4a2476143d2629761a1b",
"logs": [
{
"address": "0x9be957d1c1c1f86ba9a2e1215e9d9eefde615a56",
"topics": [
"0x86a27c2047f889fafe51029e28e24f466422abe8a82c0c27de4683dda79a0b5d"
],
"data": "0x00000000000000000000000000000000000000000000000000066d947bde761200000000000000000000000000000000000000000000000000066d947bde763a"
}
],
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x8ca0"
}
tx.origin == msg.sender statment is added to claimReward().
But we can use the V1 solution since we are EoA.
V3
https://goerli.etherscan.io/address/0xe8b7475e2790409715af793f799f3cc80de6f071#code
contract MevShareCTFMagicNumberV3 is MevShareCTFMagicNumber {
// V3 only gets one shot per tx.origin. If any tx lands that is incorrect, that tx.origin does not get another shot
mapping(address => bool) public registeredV3Attempts;
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFMagicNumber(_mevShareCaptureLogger) payable {
}
function claimReward(uint256 _magicNumber) external {
require(tx.origin == msg.sender);
require(registeredV3Attempts[tx.origin] == false);
registeredV3Attempts[tx.origin] = true;
claimRewardInternal(_magicNumber, 203);
}
}
contract MevShareCTFMagicNumber is MevShareCTFBase {
uint256 public activeBlock;
uint256 private magicNumber;
event Activate(uint256 lowerBound, uint256 upperBound);
constructor(MevShareCaptureLogger _mevShareCaptureLogger) MevShareCTFBase(_mevShareCaptureLogger) payable {
}
function activateRewardMagicNumber(uint256 _lowerBound, uint256 _upperBound, uint256 _magicNumber) external payable onlyOwner {
require (_lowerBound <= _magicNumber && _upperBound >= _magicNumber);
activeBlock = block.number;
magicNumber = _magicNumber;
emit Activate(_lowerBound, _upperBound);
}
function claimRewardInternal(uint256 _magicNumber, uint256 _captureId) internal returns (bool) {
if (activeBlock != block.number || _magicNumber != magicNumber) {
return false;
}
activeBlock = 0;
magicNumber = 0;
mevShareCaptureLogger.registerCapture(_captureId, tx.origin);
return true;
}
}
{
"hash": "0x1adbe7eccfc09c0d9a344d737d71c7c74dfee7d88c785594d72b1445f162f2a3",
"logs": [
{
"address": "0xe8b7475e2790409715af793f799f3cc80de6f071",
"topics": [
"0x86a27c2047f889fafe51029e28e24f466422abe8a82c0c27de4683dda79a0b5d"
],
"data": "0x00000000000000000000000000000000000000000000000000158d0d9733330300000000000000000000000000000000000000000000000000158d0d9733332b"
}
],
"txs": null,
"mevGasPrice": "0x2faf080",
"gasUsed": "0x8ca0"
}
We cannot use V1 solution because if we get it wrong once, we can't try it forever.
I stupidly used the same V1 and V2 codes and it became a challenge that I couldn't solve. :rofl:
As I set canRevert as True, my bundle could be processed.
After that,
my idea was that if I make many many bundles and do spamming, then the transaction that is not reverted could be processed.
bundle = {
hash,
tx of calling claimReward() and canRevert: False,
}
bundle = {
hash,
tx of calling claimReward() and canRevert: False,
}
...
bundle = {
hash,
tx of calling claimReward() and canRevert: False,
}
But I got no success.
The reason why this method was wrong was not the contract logic in which the transaction was reverted even if the magic number was wrong.. I misunderstood like V1, V2.
It may be the answer by chance.
https://github.com/y-pakorn/mev-share-ctf-rs/blob/master/src/handler.rs#L92
Every time I tried, I succeeded in half an hour.
It was not be done in Python using Threading with the same transaction configuration and bundle...
Rust's asynchronous programming seems to be excellent..
I don't know why this can be solution or not.
https://github.com/minaminao/ctf-blockchain/tree/main/src/MEVShareCTF
To use "canRevert": False
I can solve this by creating my contract that has a function that checks if I solved the problem
and if I couldn't solve it, I can revert it from the function so that the transaction is reverted.
pragma solidity 0.8.19;
interface IMevShareCaptureLogger {
function winnerCaptures (address, uint) external view returns (bool);
}
contract Checker {
IMevShareCaptureLogger public logger = IMevShareCaptureLogger(0x6C9c151642C0bA512DE540bd007AFa70BE2f1312);
address public me = 0x846603628D071EcD09b876D842a809DF2A93309B;
function checker() public view {
require(logger.winnerCaptures(me, 203)==true);
}
}
bundle = {
hash,
tx of calling claimReward() and canRevert: False,
tx of calling checker() and canRevert: False,
}
'writeups' 카테고리의 다른 글
whitehat 2023 - blockchain (0) | 2023.09.19 |
---|---|
SekaiCTF 2023 - Blockchain(The Bidding, Play for Free, Re-Remix) (0) | 2023.08.28 |
corCTF 2023 - blockchain(tribunal, baby-wallet) (0) | 2023.07.31 |
angstromctf 2023 - pwn(Sailor's Revenge) (0) | 2023.04.27 |
NumemCTF 2023 (0) | 2023.04.01 |