APWine Incorrect Check of Delegations Bugfix Review
In the Web2 world, a simple oversight in the code doesn’t always result in a huge breach of data (of course, sometimes they do). In Web3, the situation is much different. You need to stay vigilant at every step of development because a simple oversight could be dangerous.
On January 5th, whitehat setuid0 of SSLab@Gatech submitted a critical bug report with a working PoC to APWine’s bug bounty program on Immunefi. The bug was simple: in the PT tokens, one condition wasn’t checked during the burn of those tokens which could lead to the theft of the yield from the protocol after the two periods, i.e. 6 months.
Transfer of the delegation power of the governance tokens is done in the
beforeTokenTransfer() function. Due to the check of
to != address(0) inside that function, an attacker could exploit this to bypass the check for the amount of tokens put into the delegation, effectively increasing the future yield. This could be exploited because when users would withdraw their deposited PT tokens, the
PT.burnFrom() is run, which sets
to in the
For this find, the whitehat was rewarded $100,000. The APWine team was quick with the response and also with the payout of the bounty (4 days after the report was submitted to Immunefi).
How could this oversight in the code cause the yield to be stolen from the protocol?
We need to understand how the project works before we can begin explaining the bug. The APWine protocol can be used to tokenize future yields. This implies that APWine operates by storing Interest Bearing Tokens (IBT) or any other yield-bearing asset in a smart contract for a specified period of time and issuing Future Yield Tokens (FYT) in exchange.
The smart contract receives the yield created by these assets directly, and only the holders of FYTs may redeem that yield at the end of the period.
The division of a yield-bearing asset into Principal Tokens (PTs) and Future Yield Tokens is the essential functionality of APWine. A user’s deposits to the protocol are represented by the PTs. At the start of each period, the APWine generates FYT from PTs in a 1:1 ratio.
The delegation of FYT tokens is another important feature. Users that deposit PTs in APWine will receive FYT tokens on a regular basis in each new period. They can, however, delegate the generation of FYTs to another address if they want. This can be used, for example, to provide another address access to an asset’s yield without giving them access to the asset/principal itself.
Knowing all of this, we can now jump into the vulnerability itself.
Looking at the PT tokens implementation, we can see it is a standard ERC20 token with some additional logic. What we’re interested mostly in is the
if we see above is the most interesting part. The
require check makes sure that a user can’t delegate more yield than the PTs they hold and they already delegated.
if condition is making sure the update of the state is not done on the vault address and the null address. All looks good apart from the last condition i.e. not allowing the transfer of the user’s token to address zero.
As required by the ERC20 standard, a burn of tokens emits an event transferring those tokens to the zero address. This is usually implemented by calling all the hooks for the transfer, but at the end, the balance of the zero address isn’t increased. This implementation is “normal” in that respect. The
_burn function calls the above function with the following parameters
_beforeTokenTransfer(account, address(0), amount); The
to != address(0) condition is broken here during the burning of the tokens, and so the code inside an
if won’t ever be run.
The question is, how can we exploit this?
First, we need to deposit our IBTs into the
FutureVault contract. We need to do it from a fresh account. It will be explained in the next step why we need to do it. The deposit function is pretty standard, as it takes the IBTs and calculates the PT to be minted. Users that deposited PT tokens into the
FutureVault will receive FYT tokens on a regular basis in each new period.
After deposit, we can now delegate the creation of the FYT tokens to any other address. This will give access to the yield generated by the depositor to any other address. We make this delegation by calling
createFYTDelegationTo. Let’s look what is happening inside the function.
First, the function gets the total delegation amount of the depositor and compares the balance of the PT tokens to the amount to be delegated plus the amount of the already delegated tokens. Because of this, we need to make delegation from fresh accounts just to be able to bypass this check.
After delegation has been completed, we can start inflating
_receiver address in this case will always be the attacker address.
PT.burnFrom is called in the
FutureVault contract during the withdrawal process.
What’s happening in this function is a withdrawal of funds that corresponds to the PT holding of an address. We calculate how much is owed to the user with all the interest from the periods they took part in and send back the IBTs. But as the
_beforeTokenTransfer if condition will not pass, the contract fails to check if the PT balance of the user is higher or equal than the amount ready to be withdrawn, plus the PT amount delegated. This means the delegated amount of tokens from the previous step are not taken into consideration.
What we’re left with is the
totalDelegationsReceived[_receiver] intact and when an attacker waits a few periods, they can use the inflated delegation to mint PTs and FYTs and then withdraw IBTs by redeeming these tokens.
Here’s the step-by-step guide for the exploit:
- Make a deposit to the
FutureVaultcontract using deposit function
createFYTDelegationTofrom a fresh account and specify the attacker address as the recipient.
totalDelegationsReceived[_receiver]is being updated. Delegation needs to happen from the fresh account as we want to bypass the following check
- Call withdraw function redeeming PTs for IBTs. Due to the logic error in the
_beforeTokenTransfer()the check for the amount being redeemed against the redeem balance plus delegated tokens is not run. An attacker received all PTs they deposited in step 1 while keeping the delegation amount
- Repeat steps 1–3 multiple times
- Wait a few periods for the inflated delegation to generate yield
- Mint PTs and FYTs and call withdraw to redeem these tokens to IBT.
The fix was deployed to the APWine repository. It checks the delegated amount inside the
We would like to thank whitehat setuid0 of SSLab@Gatech for doing an amazing job and reporting this finding. Props also to the APWine team who did an amazing job responding quickly to the report and patching it.
This issue was reported responsibly and securely via the Immunefi platform, leading to a happy outcome for everyone, especially the users.
If you’d like to start bug hunting, we got you. Check out the Web3 Security Library, and start earning rewards on Immunefi — the leading bug bounty platform for web3 with the world’s biggest payouts.