Platform: Code4rena
Start Date: 05/07/2023
Pot Size: $390,000 USDC
Total HM: 136
Participants: 132
Period: about 1 month
Judge: LSDan
Total Solo HM: 56
Id: 261
League: ETH
Rank: 42/132
Findings: 6
Award: $1,199.83
🌟 Selected for report: 0
🚀 Solo Findings: 1
🌟 Selected for report: KIntern_NA
698.0846 USDC - $698.08
https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/governance/twTAP.sol#L498-L520 https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/governance/twTAP.sol#L514 https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/tokens/BaseTapOFT.sol#L163-L196 https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/tokens/BaseTapOFT.sol#L198-L245
Possible to withdraw more expensive funds from twTAP
contract.
For, example, we observes that, pool rewards with shitcoin and USDT, then, we can make a request so that we extract all USDT from the contract.
Due to incorrect validation
To process this exploit need to use TapOFT
contract (which is implementation of BaseTapOFT
).
tapOFT
tokens on twTAP
contract with function participate
. -- We receive a tokenId
claimRewards
on TapOFT
contract. This function implements cross-chain call with passed parameters. Please, note -- here we pass address[] memory rewardTokens,
which just encode as it is, No any additional checks._nonblockingLzReceive
on destination chain. In turn, this decodes passed data and call _claimRewards
._claimRewards
decodes passed payload --( .... IERC20[] memory rewardTokens, ... ) = abi.decode( _payload, ( .... IERC20[],... ) );
Note, we decodes IERC20[] memory rewardTokens,
, then this array of tokens passed to twTap.claimAndSendRewards(tokenID, rewardTokens)
.
_claimRewardsOn
, this function accepts IERC20[] memory _rewardTokens
as it was created on Source chain, without any checks._claimRewardsOn
we at the first calls claimable
-- uint256[] memory amounts = claimable(_tokenId);
-- this returns array of tokens amount which might be claimed. (i.e. [100, 200, 300] -- means, you may receive 100 tokens for token which has index 0 in map rewardTokenIndex
)uint256 len = _rewardTokens.length; for (uint256 i = 0; i < len; ) { uint256 claimableIndex = rewardTokenIndex[_rewardTokens[i]]; uint256 amount = amounts[i]; if (amount > 0) { // Math is safe: `amount` calculated safely in `claimable()` claimed[_tokenId][claimableIndex] += amount; rewardTokens[claimableIndex].safeTransfer(_to, amount); } ++i; }
And here:
uint256 claimableIndex = rewardTokenIndex[_rewardTokens[i]]; uint256 amount = amounts[i];
We may chose which token use, but amount calculated by contract. Therefore, if for example, in contract registered:
we can pass to TapOFT#claimRewards
tokens like: [1, 1, 1] and in _claimRewardsOn
token 1 will receive payments thee times a row, thats wrong and incorrect.
Please, note:
A similar attack can be made if we pass an array of non-existent tokens,
then this:
uint256 claimableIndex = rewardTokenIndex[_rewardTokens[i]];
will return us the index 0 because this is how the storage in EVM works and we can call the token transfer with the index 0 several times.
Manual review
Access Control
#0 - c4-pre-sort
2023-08-07T03:42:44Z
minhquanym marked the issue as primary issue
#1 - c4-pre-sort
2023-08-07T03:43:57Z
minhquanym marked the issue as duplicate of #1304
#2 - c4-judge
2023-09-22T12:44:28Z
dmvt marked the issue as satisfactory
174.5212 USDC - $174.52
https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/options/TapiocaOptionLiquidityProvision.sol#L276-L293 https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/options/TapiocaOptionLiquidityProvision.sol#L297-L329 https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/options/TapiocaOptionLiquidityProvision.sol#L126-L143
Possible to register assetID
twice with different singularity
.
In TapiocaOptionLiquidityProvision
let's consider registerSingularity
This function only validates singularity
uniqueness.
But do not validate assetID
uniqueness.
So, we can call registerSingularity
twice with same assetID
. And singularities
array will contains the same assetID
twice.
When we decide to remove singularity we call unregisterSingularity
and iterates over _singularities
, And, therefore, we delete only first assetID
occurrence, but the last one will be in this array.
Moreover we can't call unregisterSingularity
because singularity
will be removed, and assetID
will point out to zero element.
This lead to case, when someone calls getSingularityPools
he may get pool information which contains zeros.
Because this construction
pools[i] = activeSingularities[sglAssetIDToAddress[_singularities[i]]];
will be point out to zero memory.
Therefore, user may observe incorrect data, which will be impossible to fix.
Manual review
Add uniqueness constraint to assetID
Invalid Validation
#0 - c4-pre-sort
2023-08-07T04:21:46Z
minhquanym marked the issue as low quality report
#1 - minhquanym
2023-08-07T04:22:02Z
onlyOwner func. Consider QA
#2 - c4-pre-sort
2023-08-09T07:39:47Z
minhquanym marked the issue as primary issue
#3 - c4-judge
2023-09-20T15:24:26Z
dmvt changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-09-20T15:38:12Z
dmvt marked the issue as grade-c
#5 - c4-judge
2023-09-20T15:40:49Z
This previously downgraded issue has been upgraded by dmvt
#6 - c4-judge
2023-09-20T15:43:59Z
dmvt marked the issue as partial-50
#7 - dmvt
2023-09-20T15:44:19Z
really hard to read, but correct
#8 - c4-judge
2023-09-20T15:45:02Z
dmvt marked issue #1350 as primary and marked this issue as a duplicate of 1350
76.3356 USDC - $76.34
https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/option-airdrop/AirdropBroker.sol#L84 https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/option-airdrop/AirdropBroker.sol#L459-L461
Users participated in phase 3 of AirdropBroker
will receive only 714 tokens. But at the same time Token has 18 decimals. So this phase is wrong.
This is typo, But it's critical typo, because on phase3 users will receive 714 tokens it's 1e16 times less than single token.
uint256 public constant PHASE_3_AMOUNT_PER_USER = 714;
uint128 expiry = uint128(lastEpochUpdate + EPOCH_DURATION); // Set expiry to the end of the epoch uint256 eligibleAmount = PHASE_3_AMOUNT_PER_USER; uint128 discount = uint128(PHASE_3_DISCOUNT); oTAPTokenID = aoTAP.mint(msg.sender, expiry, discount, eligibleAmount);
Manual review
Fix with: uint256 public constant PHASE_3_AMOUNT_PER_USER = 714 * 1e18;
Error
#0 - c4-pre-sort
2023-08-05T15:10:10Z
minhquanym marked the issue as duplicate of #173
#1 - c4-judge
2023-09-18T13:28:00Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-09-18T13:29:30Z
dmvt marked the issue as satisfactory