<-->

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,
}

 

+ Recent posts