Platform: Code4rena
Start Date: 09/09/2022
Pot Size: $42,000 USDC
Total HM: 2
Participants: 101
Period: 3 days
Judge: hickuphh3
Total Solo HM: 2
Id: 161
League: ETH
Rank: 1/101
Findings: 2
Award: $18,947.65
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: rvierdiiev
18900 USDC - $18,900.00
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/redeem/TribeRedeemer.sol#L58 https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/redeem/TribeRedeemer.sol#L70
TribeRedeemer
contract has one goal. It should take _redeemedToken
(token that contract will accept from users), _tokensReceived
- list of exchange tokens(this are created exactly to be changed for redeemedToken) and _redeemBase
amount of tokens that should be redeemed(this actually should be IERC20(_redeemedToken).totalSupply())
. After that it will start redeeming _redeemedToken
in exchange to _tokensReceived
tokens.
Suppose we have redeemed token tokenA
with total supply of 10000
and redeemBase == 10000
. And in _tokensReceived
list we have only 1 token tokenB
with total supply of 10000
(all tokens are controlled by TribeRedeemer
). According to logic of TribeRedeemer
if user wants to redeem X tokens then he will receive (x * tokenB.balanceOf(address(this))) / redeemBase
that in our case will be just amount X. So user send X tokenA
and receive back X tokenB
. Now because redeemBase == 10000
and contract balance of tokenB
is 10000
the exchange ratio is 1:1
.
However if someone will transfer some amount of tokenA
to TribeRedeemer
contract directly using ERC20.transfer
for example 500
tokens then it not call redeem
function and redeemBase
value will not be decreased with amount sent. That means that right now the exchange ratio should not be 1:1
as TribeRedeemer
contract received more 500
tokens and didn't pay user for them(it should redeem that amount then with the next users, they share should be increased). So the next users will receive less amount then they should(TribeRedeemer
should spend all tokenB
amount in exchange of all tokenA
tokens).
Here is where amounOut
is calculated.
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/redeem/TribeRedeemer.sol#L58
This is where the redeemBase
is decreased with redeem amount, but is not called because of direct transfer.
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/redeem/TribeRedeemer.sol#L70
Do not use redeemBase
for calculation amount of tokens that are left for redeeming. Use IERC20(_redeemedToken).totalSupply()) - IERC20(_redeemedToken).balanceOf(address(this))
.
#0 - thomas-waite
2022-09-19T14:00:51Z
Any redeemedToken
s sent to this contract will be locked and effectively burned. They are not to be used in the accounting of redemptions or redeemBase
#1 - HickupHH3
2022-09-30T02:57:56Z
Hmmm, the warden has a point actually. Currently, the rewards tied to TRIBE tokens that were sent directly to the contract instead of via redeem()
becomes unrecoverable (burnt) as well.
It's not a bad suggestion; it at least helps to split the rewards of directly sent TRIBE tokens with the remaining TRIBE holders, which acts as a form of reward recovery.
The recommended mitigation is good, but perhaps modified to be an immutable value / constant _redeemBase
, then the calculation becomes
uint256 redeemedAmount = (amountIn * balance) / (_redeemBase - IERC20(_redeemedToken).balanceOf(address(this));
Keeping the medium severity as it's an indirect loss of rewards that can be recovered / re-distributed.
#2 - thomas-waite
2022-10-18T13:55:37Z
@HickupHH3 All TRIBE tokens have an equal claim to the funds that are locked in this contract, irrespective of what other TRIBE tokens do.
The situation described is just a special case of TRIBE tokens being locked out of circulation. It's not feasible for this contract to account for all locations where user circulating TRIBE is subsequently locked/removed from circulation and nor is it particularly desired - all TRIBE has an equal claim to the funds held on the contract. Dispute that this is an issue
#3 - HickupHH3
2022-10-18T21:44:15Z
Agree with the intention to keep all claims equal, and that it's not feasible to handle all outlier cases.
Implementing the suggestion would change user behaviour to claim as late as possible, since the TRIBE amount that's mistakenly sent only increases over time.
Downgrading to QA.
#4 - HickupHH3
2022-10-19T02:22:56Z
part of #121
#5 - rvierdiyev
2022-10-19T09:40:59Z
First of all i would like to say that current implementation of TribeRedeemer is not going to pay redeemers equal claims. This is important because the sponsor argues that if we change the implementation like i have suggested in submission, then next users will get more rewards. Also HickupHH3 said that this incentivize people to call redeem as late as possible to get bigger reward.
I will explain why current implementation is not going to pay equal claims. This is because you can top up any exchange tokens directly(transfer to TribeRedeemer) and then next users will get bigger share than previous, because the formula uses exchange token balance to calculate reward amount. uint256 redeemedAmount = (amountIn * balance) / base; where balance is uint256 balance = IERC20(tokensReceived[i]).balanceOf(address(this));
So the assumption of sponsor about equal claims is not true. And also HickupHH3 assumption that my proposed change will push users to redeem tokens later is partly wrong. They are incentivized to do so already in current implementation(as someone can transfer some exchange token to the TribeReddemer). Why i suggested to use redeem token balance, because i saw in the code that TribeRedeemer is going to spend all exchange tokens for the redeemed tokens.
All exhange tokens(tokensReceived in the code) are sent to the TribeRedeemer contract before and there is no way for TribeRedeemer to return any funds that are still controlled by contract. That means that the purpose of TribeRedeemer is to distribute all exchange tokens to the redeemers(there is no logic to leave some part of that tokens locked in the contract, it's better to distribute everything).
Let's see 3 situations.
I hope, that i showed that current implementation can't guarantee equal rewards for users and it will be logical to use the way that i proposed to not just burn exchange tokens but distribute them among redeemers. If sponsor wants to pay equal claims, then he need to initialize TribeRedeemer with one more param exchangeTokensPerRedeemToken array where he can provide fixed amount of tokens to pay for redeemed token and use smth like this uint256 redeemedAmount = (amountIn * exchangeTokensPerRedeemToken) / base;
only in this case it's possible to get users equal claims
#6 - HickupHH3
2022-10-20T08:26:27Z
In the course of my investigation to see if a user has accidentally directly sent TRIBE to the redeemer contract, I have discovered that what @rvierdiyev said about equal claims is true:
And also HickupHH3 assumption that my proposed change will push users to redeem tokens later is partly wrong. They are incentivized to do so already in current implementation(as someone can transfer some exchange token to the TribeReddemer).
The RGT reserve and timelock sent an additional ~430k DAI to the redeemer contract recently, after redemptions have begun:
This user who redeemed close to 17M TRIBE missed out on about 16856213429000000000000000 / 458964340000000000000000000 * 430000 ~= 15792.45
additional DAI.
Hence, it is a fact that not all claims are equal; later claims have already benefitted from additional tokens that were sent to the redeemer contract.
I've flipped flopped on my decision, but in light of what I found, my rationale for the downgrade no longer stands.
Implementing the suggestion would change user behaviour to claim as late as possible, since the TRIBE amount that's mistakenly sent only increases over time.
As @rvierdiyev pointed out, this is already true, although I would argue that implementing this suggestion would worsen the problem.
The situation described is just a special case of TRIBE tokens being locked out of circulation. It's not feasible for this contract to account for all locations where user circulating TRIBE is subsequently locked/removed from circulation and nor is it particularly desired.
I agree that the protocol should not be expected to handle all outlier cases; and that accidentally sending tokens to the redeemer contract is a special case. However, it is precisely because it is a boundary / special case that the protocol is able to handle and remedy; it most cases, it doesn't have the ability to do so. It's akin to token contracts blocking address(this)
in to
addresses. If a path exists to recover rewards that would've been lost together with the TRIBE tokens by redistributing it, why not?
Keeping the medium severity as it fulfils the condition: protocol leaking value (rewards lost that in this case can be remedied) with a hypothetical attack path with stated assumptions, but external requirements. This vulnerability can also be classified as a user-error bug, which have been awarded up to medium severity in previous contests Eg. overpaying with ETH.
🌟 Selected for report: GalloDaSballo
Also found by: 0x040, 0x1f8b, 0x4non, 0x52, 0x85102, 0xNazgul, 0xSky, 0xSmartContract, Aymen0909, Bnke0x0, CertoraInc, Chandr, Chom, CodingNameKiki, Deivitto, Diana, Funen, JC, Jeiwan, Junnon, KIntern_NA, Lambda, Mohandes, Noah3o6, Ocean_Sky, Picodes, R2, Randyyy, RaymondFam, ReyAdmirado, Rohan16, Rolezn, Samatak, Sm4rty, SnowMan, SooYa, StevenL, Tagir2003, Tointer, TomJ, Tomo, V_B, Waze, _Adam, __141345__, a12jmx, ajtra, ak1, asutorufos, bharg4v, bobirichman, brgltd, c3phas, cccz, cryptonue, cryptostellar5, cryptphi, csanuragjain, d3e4, datapunk, delfin454000, dipp, djxploit, durianSausage, erictee, fatherOfBlocks, gogo, got_targ, hansfriese, horsefacts, hyh, ignacio, innertia, izhuer, karanctf, ladboy233, leosathya, lucacez, lukris02, mics, oyc_109, pashov, pauliax, prasantgupta52, rbserver, ret2basic, rfa, robee, rokinot, rotcivegaf, rvierdiiev, sach1r0, scaraven, sikorico, simon135, smiling_heretic, sorrynotsorry, unforgiven, wagmi, yixxas
47.6531 USDC - $47.65
cTokenExchangeRates[cToken]
is not null and revert instead of providing result of 0. This can lead to misunderstand
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L85_cTokens[i]
for 0 address.
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L133
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L133cToken
for 0 address.
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/fuse/RariMerkleRedeemer.sol#L201amountIn
is bigger then 0.
https://github.com/code-423n4/2022-09-tribe/blob/main/contracts/shutdown/redeem/TribeRedeemer.sol#L64