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:
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: