Unlock the vault to pass the level!
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
Upon construction, locked
is set to true, and a random bytes32 password
is generated. The unlock
function can be called to set locked
to false if the password
is correct (which is impossible without knowning the actual password, since we cannot brute force the password easily).
The vulnerability is that the password
is not actually private
as it claims to be. I have read a nice article about accessing private data in smart contracts. I will do a quick summary here for learning purposes.
- State Variable Visibility
public
: the variable can be accessed by any contract or external account.private
: the variable can only be accessed by the contract that defines it.internal
: the variable can only be accessed by the contract that defines it, and by contracts that inherit from it.
- Storage layout in EVM
The EVM stores smart contract state variables in the order that they were declared in slots on the blockchain. The default value of each slot is always 0. Each memory slot can hold up to 32 bytes of data.
Similar to how C++ struct handles memory, the EVM will try to pack variables into the same slot if possible. If two or more variables fit into a single 32-byte slot, they are packed into the same slot, beginning on the right.
Now, you can access a private variable by
- Get the slot you want to access, e.g. slot 1
- Use any library (e.g.
ethers.js
) to read the memory slots of the contract on the blockchain. e.g.await ethers.provider.getStorageAt(contract_address, 1);
- Decode the returned hex encoded value to get the value of the private variable.
In Devtool,
> await web3.eth.getStorageAt(await contract.address, 0)
'0x0000000000000000000000000000000000000000000000000000000000000001' # locked = true
> await web3.eth.getStorageAt(await contract.address, 1)
'0x412076657279207374726f6e67207365637265742070617373776f7264203a29' # password
Decode the hex gives password A very strong secret password :)
. Now we just call unlock
with this password. Note that bytes32
can be passed as hex, so you don't really need to actually see the plaintext password :)
> await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29');
{tx: '0x42808f7a2b97bae94fb60ab7d711fb466ec3ab7023a67a3297f71a1aa843c8cb', receipt: {…}, logs: Array(0)}
Finally, submit the instance to pass the level.
It's important to remember that marking a variable as private only prevents other contracts from accessing it. State variables marked as private and local variables are still publicly accessible.
To ensure that data is private, it needs to be encrypted before being put onto the blockchain. In this scenario, the decryption key should never be sent on-chain, as it will then be visible to anyone who looks for it.