Sandclock contest - kenzo's results

The Next Generation of Wealth Creation.

General Information

Platform: Code4rena

Start Date: 06/01/2022

Pot Size: $60,000 USDC

Total HM: 20

Participants: 33

Period: 7 days

Judge: LSDan

Total Solo HM: 9

Id: 67

League: ETH

Sandclock

Findings Distribution

Researcher Performance

Rank: 7/33

Findings: 5

Award: $2,604.78

🌟 Selected for report: 3

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: camden

Also found by: Ruhum, WatchPug, cccz, cmichel, danb, defsec, harleythedog, hyh, kenzo, leastwood, palina, pauliax, pmerkleplant, ye0lde

Labels

bug
duplicate
3 (High Risk)

Awards

90.0579 USDC - $90.06

External Links

Handle

kenzo

Vulnerability details

To calculate the total vault\strategy's assets, the strategy is using a spot price: curvePool.get_dy_underlying. This can be easily manipulatable by an exploiter (including using flash loans).

Impact

Wrong accounting:

  • If exploiter temporarily decreases get_dy_underlying before depositing, he would get more shares than he deserves. He can then withdraw other user's funds. This can also prevent a real user from withdrawing his funds.
  • If exploiter temporarily increases get_dy_underlying before claiming yield, he would get more yield than really exists. This might come on the expense of legitimate user's investment funds, and the user won't be able to fully withdraw until (and if) the vault makes enough real yield to cover the manipulation.

Proof of Concept

When I say that an exploiter can temporarily change the price, I mean that he can use flash loan funds (or his own funds) to execute a large trade on Curve before calling the vault (in the same tx). This will incur a slippage of price, that the vault will use as if it's the real price. After the vault action, the exploiter can swap back on Curve.

If exploiter temporarily decreases get_dy_underlying before depositing: Let's take this scenario:

  • Alice deposits 100 USDT tokens
  • Alice's funds are converted to UST and deposited into EthAnchor
  • Bob manipulates Curve's get_dy_underlying such that the value of the 100 UST tokens is now 90 UST tokens. He then deposits 100 USDT.

At this point, when depositing Bob's funds, the shares mechanism would call get_dy_underlying to calculate the total underlying, and will think that the vault is in a loss. It would give Bob shares, and mint global shares, as if the vault's underlying is 90, although it is actually 100.

Now let's say Alice tries to withdraw - The shares mechanism would look at the vault's balance and will see that it has increased by 10 since Bob deposited. Since (at Bob's deposit) Sandclock minted global shares as if Alice's underlying is only 90, of these "new" 10 tokens that Sandclock now sees, only (90 / 190)*10 = 4.7 belong to Alice. So Sandclock thinks that Alice is in a loss and can not withdraw her funds yet, although that is false. So Alice can not withdraw her funds.

Now let's say Bob claims yield - As mentioned before, the shares mechanism will calculate that of the 10 "new" underlying, 100/190 belong to Bob. Bob seems to be in profit, so he will be able to claim "his share" of the "yield" - which is actually partly Alice's investment.

[Note: this scenario can be easily verified by adding a function to the vault that will transfer underlying out before Bob deposits. This can be considered to simulate a decreasing get_dy_underlying and therefore smaller totalUnderlying. Then after Bob deposited, you can mint underlying back to the vault to simulate the price getting back to normal.]

If exploiter temporarily increases get_dy_underlying before claiming yield: In this scenario, both Alice and Bob the exploiter (not Bob the builder) have deposited as normal. Bob then manipulatively increases get_dy_underlying before claiming yield. The shares mechanism will think there is yield to claim, which it will send to Bob. But this yield is actually Alice and Bob's investment. This process can then be repeated. So again, Bob is getting funds which are not his. Damn it, Bob.

[Note: this scenario can be verified by adding a global variable uint256 manipulation, and adding it to totalUnderlying()'s calculation. Then set it to some amount like 10 ether before Bob claims his yield, and set it back to 0 after. This will simulate manipulation of the strategy's invested assets.]

Don't use a plain spot price as price feed. Use some kind of Oracle.

#0 - naps62

2022-01-11T18:44:13Z

duplicate of #7

Findings Information

Awards

90.0579 USDC - $90.06

Labels

bug
duplicate
3 (High Risk)

External Links

Handle

kenzo

Vulnerability details

Upon withdrawal of deposit, the claimer will be called with onDepositBurned. This happens after the claimer shares have been updated, but before the underlying has been sent away from the contract. Therefore the claimer can reenter the contract, at an intermediary state where the invariants are not held.

Impact

Claimer can claim more yield than he deserves.

Proof of Concept

The withdraw function iterates on all deposits, and then sends underlying only at the end. For each deposit (while iterating), Sandclock first updates the shares state, and then calls onDepositBurned. So the claimer can reenter the vault after the shares have been updated but before underlying was sent.

POC for exploit:

  • Alice deposits 100 tokens, with claims that go 50% to bob and 50% to exploiter
  • Vault generates 10 yield
  • Alice withdraws the deposit which has exploiter as claimed
  • The exploiter, on onDepositBurned, claims his yield from the contract. Since the underlying has not been sent yet, the state is not consistent. Exploiter gets 9.16 yield instead of 5.

Usual reentrancy protection: conform with checks-effects-interaction pattern, or add reentrancy lock.

#0 - r2moon

2022-01-11T16:01:30Z

Findings Information

🌟 Selected for report: kenzo

Labels

bug
2 (Med Risk)
sponsor confirmed
sponsor strategy

Awards

1771.4916 USDC - $1,771.49

External Links

Handle

kenzo

Vulnerability details

EthAnchor's docs state that "the contract address of ExchangeRateFeeder may change as adjustments occur". BaseStrategy does not have a setter to change exchangeRateFeeder after deployment.

Impact

Inaccurate/unupdated values from exchangeRateFeeder when calculating vault's total invested assets.

While the strategy's funds could be withdrawn from EthAnchor and migrated to a new strategy with correct exchangeRateFeeder, during this process (which might take time due to EthAnchor's async model) the wrong exchangeRateFeeder will be used to calculate the vault's total invested assets. (The vault's various actions (deposit, claim, withdraw) can not be paused.)

Proof of Concept

The exchangeRateFeeder is being used to calculate the vault's invested assets, which is used extensively to calculate the correct amount of shares and amounts: (Code ref)

function investedAssets() external view virtual override(IStrategy) returns (uint256) { uint256 underlyingBalance = _getUnderlyingBalance() + pendingDeposits; uint256 aUstBalance = _getAUstBalance() + pendingRedeems; return underlyingBalance + ((exchangeRateFeeder.exchangeRateOf(address(aUstToken), true) * aUstBalance) / 1e18); }

EthAnchor documentation states that unlike other contracts, exchangeRateFeeder is not proxied and it's address may change in future: "the contract address of ExchangeRateFeeder may change as adjustments occur. " (ref)

Add a setter for exchangeRateFeeder.

Findings Information

🌟 Selected for report: kenzo

Also found by: danb, harleythedog

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

478.3027 USDC - $478.30

External Links

Handle

kenzo

Vulnerability details

A vault wouldn't let the strategy be changed unless the strategy holds no funds. Since anybody can send funds to the strategy, a griefing attack is possible.

Impact

Strategy couldn't be changed.

Proof of Concept

setStrategy requires strategy.investedAssets() == 0. (Code ref) investedAssets contains the aUST balance and the pending redeems: (Code ref)

uint256 aUstBalance = _getAUstBalance() + pendingRedeems;

So if a griefer sends 1 wei of aUST to the strategy before it is to be replaced, it would not be able to be replaced. The protocol would then need to redeem the aUST and wait for the process to finish - and the griefer can repeat his griefing. As they say, griefers gonna grief.

Consider keeping an internal aUST balance of the strategy, which will be updated upon deposit and redeem, and use it (instead of raw aUST balance) to check if the strategy holds no aUST funds. Another option is to add capability for the strategy to send the aUST to the vault.

#0 - CloudEllie

2022-01-10T15:19:29Z

Warden kenzo requested that I add the following:

"Additionally, impact-wise: EthAnchor does not accept redeems of less than 10 aUST. This means that if a griefer only sends 1 wei aUST, the protocol would have to repeatedly send additional aUST to the strategy to be able to redeem the griefer's aUST."

Findings Information

🌟 Selected for report: Dravee

Also found by: 0x1f8b, camden, cccz, certora, defsec, jayjonah8, kenzo, p4st13r4, palina, pauliax, robee

Labels

bug
duplicate
1 (Low Risk)
sponsor confirmed
sponsor vault

Awards

15.442 USDC - $15.44

External Links

Handle

kenzo

Vulnerability details

This is totalUnderlyingMinusSponsored: (Code ref)

function totalUnderlyingMinusSponsored() public view returns (uint256) { // TODO no invested amount yet return totalUnderlying() - totalSponsored; }

The comment implies that invested amounts are not taken into consideration. However, looking at totalUnderlying(), we can see that they are. (Code ref)

return underlying.balanceOf(address(this)) + strategy.investedAssets();

So this comment is incorrect and can be removed.

Similarly, in computeAmount, there is code and comment: (Code ref)

} else { // TODO exclude sponsored assets return ((_totalUnderlyingMinusSponsored * _shares) / _totalShares);

However, the sponsored assets are already removed (as we can see from the name and totalUnderlyingMinusSponsored() above). So this comment is also inaccurate and can be removed.

#0 - dmvt

2022-01-28T14:41:09Z

duplicate of #96

Findings Information

🌟 Selected for report: kenzo

Also found by: p4st13r4, palina

Labels

bug
1 (Low Risk)
sponsor confirmed
sponsor vault

Awards

159.4342 USDC - $159.43

External Links

Handle

kenzo

Vulnerability details

This is Depositors onlyOwner modifier: (Code ref)

modifier onlyVault() { require(msg.sender == vault, "Claimers: not authorized"); _; }

The "Claimers" in the message needs to be replaced with "Depositors".

#0 - dmvt

2022-01-29T12:53:56Z

I'm changing this to low risk because it can result in confusion for the end user when encountered and therefor counts as a comment issue.

#1 - naps62

2022-02-16T15:52:00Z

already fixed

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter