// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
이 문제는 delegatecall에 대해서 알고 있어야 한다. delegatecall은 다른 컨트렉트에 있는 함수를 호출할 수 있는데 call로 호출을할 경우 호출되는 해당 컨트렉트에 storage에 기록되는 반면, delegatecall은 해당 함수가 마치 delegatecall을 호출한 컨트렉트에 구현되어있는 것처럼 delegatecall을 호출한 컨트렉트의 storage가 덮어 씌워지는 방식이다.
여기서 그러면 의문이 생길 수도 있다. 이 문제에서는 delegate 컨트렉트에 pwn()
함수에서는 owner=msg.sender
를 수행한다. 그런데 delegatecall을 호출하는 delegation 컨트렉트에는 owner
가 존재하여 delegation 컨트렉트에 선언된 owner
에 써진다고 생각할 수 있다. 하지만 이것은 엄밀히 말하면 아니다.
EVM에서 storage는 슬롯 단위로 데이터가 저장되는데 선언한 변수에 순서대로 슬롯 번호를 부여받는다. delegate 컨트렉트와 delegation 컨트렉트는 모두 처음에 선언한 변수가 owenr
이기 때문에 둘다 storage 슬롯 0번을 부여받는다. 예를들어 delegation 컨트렉트에 owner
보다 먼저 선언된 변수가 존재한다면 그 변수가 슬롯이 0이될 것이고 owner
변수는 슬롯 1번이될 것이다. 결론적으로 delegatecall은 storage에 접근할 때 변수명으로 접근하는 것이아니라 슬롯 번호로 접근한다.
cast send --rpc-url $G_RPC --private-key $P_KEY 0x8c4Bc1f9e5b608972E83780406F5D64B8d1c8Ede "pwn()" --gas-limit 100000
결론적으로는 fallback()
을 호출한 msg.data
가 delegatecall로 호출되는데 pwn()
함수를 호출하게되면 storage 슬롯 0번에 접근되어 갚이 덮어씌워지기 때문에 foundry로 호출하여 문제를 해결하였다.