SYFI rebase bug: 200$ to 29k$

This morning, Yannick(twitter:@YannickCrypto) sent a tx link showing that some guy made about 29k$ from a 200$ trade. And it’s probably a bug of SYFI rebase logic. YAM happened to have a rebase logic last month and my company Peckshield has a blog post on it(post here). So I spent some time this afternoon to read the source code of YAM and uniswap to understand the rebase logic. Obviously, the bug in SYFI is that it didn’t call the uniswap pool’s sync() function after rebase.

Let’s take a look at the two txs first.

https://etherscan.io/tx/0xed33e727dd5b2f8e5164f6e15dabc1923652f2e933645378a87c45bf33c4e59a

This tx, the guy swapped 0.5 Ether for 2.014585411 SYFI. And after rebase, he got 15,551.072978099 SYFI, then he just swapped all the SYFI for eth and got 747 Ether here.

https://etherscan.io/tx/0xbb45a3aaa222432f50974b4be0852445e446698d33b0fcd47a4f627a2764ea83

Since I can’t find the source code of the SYFI’s rebaser. Let’s take a look at YAM’s rebaser.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function rebase() public
{
// EOA only
require(msg.sender == tx.origin);
// ensure rebasing at correct time
_inRebaseWindow();

// This comparison also ensures there is no reentrancy.
require(lastRebaseTimestampSec.add(minRebaseTimeIntervalSec) < now);

// Snap the rebase time to the start of this window.
lastRebaseTimestampSec = now.sub(
now.mod(minRebaseTimeIntervalSec)).add(rebaseWindowOffsetSec);

epoch = epoch.add(1);

// get twap from uniswap v2;
uint256 exchangeRate = getTWAP();

// calculates % change to supply
(uint256 offPegPerc, bool positive) = computeOffPegPerc(exchangeRate);
// postive true rate > targetrate
uint256 indexDelta = offPegPerc;

// Apply the Dampening factor.
indexDelta = indexDelta.div(rebaseLag);

YAMTokenInterface yam = YAMTokenInterface(yamAddress);

if (positive) {
require(yam.yamsScalingFactor().mul(uint256(10**18).add(indexDelta)).div(10**18) < yam.maxScalingFactor(), "new scaling factor will be too big");
}


uint256 currSupply = yam.totalSupply();

uint256 mintAmount;
// reduce indexDelta to account for minting
if (positive) {
uint256 mintPerc = indexDelta.mul(rebaseMintPerc).div(10**18);
indexDelta = indexDelta.sub(mintPerc);
mintAmount = currSupply.mul(mintPerc).div(10**18);
}

// rebase
uint256 supplyAfterRebase = yam.rebase(epoch, indexDelta, positive);
assert(yam.yamsScalingFactor() <= yam.maxScalingFactor());

// perform actions after rebase
afterRebase(mintAmount, offPegPerc);
}

Here, the rebaser will get the uniswap pool’s exchange rate exchangeRate by calling the function getTWAP. Then it’s able to compare the exchangeRate with target rate to get the indexDelta which will be used in Yam token’s rebase function to change the totalSupply. The step that I want to point out here is that the rebaser will call afterRebase at the end.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function afterRebase(
uint256 mintAmount,
uint256 offPegPerc
)
internal
{
// update uniswap
UniswapPair(uniswap_pair).sync();

if (mintAmount > 0) {
buyReserveAndTransfer(
mintAmount,
offPegPerc
);
}

// call any extra functions
for (uint i = 0; i < transactions.length; i++) {
Transaction storage t = transactions[i];
if (t.enabled) {
bool result =
externalCall(t.destination, t.data);
if (!result) {
emit TransactionFailed(t.destination, i, t.data);
revert("Transaction Failed");
}
}
}
}

In this funciton, the target uniswap pool’s sync() function will be called. In UniswapV2Pair.sol, the sync() function will update the token balance of the pool.

1
2
3
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}

It’s clear that the SYFI rebaser didn’t call thie sync() function, and the guy’s SYFI balance increased then he just got the eth in the pool.