Where the Money Went?
Why blind calldata forwarding breaks DeFi vaults
This week we talked about how contests changed and why you need sharper attacking instincts.
Time to test yours.
BasketRebalancer is an on-chain index fund.
It holds ERC-20 tokens weighted by basis points.
A governor can rebalance the basket
by selling holdings into base,
then buying new assets.
Find the vulnerability.
Answer will be dropped next Monday 🔥
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract SimpleDex {
address public owner;
uint256 public rate = 1e18;
constructor() { owner = msg.sender; }
function setRate(uint256 r) external { require(msg.sender == owner); rate = r; }
function swap(address tIn, address tOut, uint256 amtIn, address to) external returns (uint256 out) {
Token(tIn).transferFrom(msg.sender, address(this), amtIn);
out = (amtIn * rate) / 1e18;
require(out > 0);
Token(tOut).transfer(to, out);
}
}
contract BasketRebalancer {
uint256 constant BPS = 10_000;
address public governor;
address public base;
address public dex;
address[] public holdings;
uint256[] public bps;
constructor(address _gov, address _base, address _dex) {
governor = _gov; base = _base; dex = _dex;
}
function setComposition(address[] calldata a, uint256[] calldata b) external {
require(msg.sender == governor);
require(a.length == b.length && a.length > 0);
uint256 t;
for (uint256 i; i < b.length;) { require(b[i] > 0); t += b[i]; unchecked { ++i; } }
require(t == BPS);
holdings = a; bps = b;
}
function deposit(address token, uint256 amount) external {
Token(token).transferFrom(msg.sender, address(this), amount);
}
function rebalance(
address[] calldata newAssets,
uint256[] calldata newBps,
bytes[] calldata sellData,
bytes[] calldata buyData,
uint256[] calldata allocateAmounts
) external {
require(msg.sender == governor);
require(newAssets.length == newBps.length && newAssets.length > 0);
require(buyData.length == newAssets.length);
require(allocateAmounts.length == newAssets.length);
uint256 t;
for (uint256 i; i < newBps.length;) { require(newBps[i] > 0); t += newBps[i]; unchecked { ++i; } }
require(t == BPS);
// Phase 1: sell all current holdings → base
uint256 baseReceived;
for (uint256 i; i < holdings.length;) {
address asset = holdings[i];
uint256 bal = Token(asset).balanceOf(address(this));
if (bal > 0) {
if (asset == base) {
baseReceived += bal;
} else {
uint256 before = Token(base).balanceOf(address(this));
Token(asset).approve(dex, bal);
(bool ok,) = dex.call(sellData[i]);
require(ok);
baseReceived += Token(base).balanceOf(address(this)) - before;
}
}
unchecked { ++i; }
}
// Phase 2: buy new assets with base
if (baseReceived > 0) {
for (uint256 j; j < newAssets.length;) {
Token(base).approve(dex, allocateAmounts[j]);
(bool ok,) = dex.call(buyData[j]);
require(ok);
unchecked { ++j; }
}
holdings = newAssets;
bps = newBps;
}
}
}
