Platform: Code4rena
Start Date: 23/11/2022
Pot Size: $24,500 CANTO
Total HM: 5
Participants: 37
Period: 5 days
Judge: berndartmueller
Total Solo HM: 2
Id: 185
League: ETH
Rank: 1/37
Findings: 3
Award: $4,480.36
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: Ruhum
Also found by: hihen, ronnyx2017
9421.864 CANTO - $1,521.63
The incorrect way revenue is calculated can lead to CSR being stolen through proxy attacks, which is likely to lead the ecology into CSR bribery war.
Eventually, this feature will translate into reduced gas fees for all transactions, regardless of whether contracts are registered for the CSR or not.
This amounts to a complicated way to change the transaction gas fee calculation formula. A discount for everyone is a discount for no one.
In PostTxProcessing, the CSR fee is sent to the nftID
associated with the contract msg.To()
.
This means, in the case of a transaction with external calls EOA -> C1 -> C2 -> C3 ...
, contract C1 will get the entire CSR fee of the transaction.
For any existing contract or application, an attacker can deploy a proxy contract to capture all the CSR fees that would otherwise belong to that contract / application developer. The proxy contract will register with csr and use part of the revenue to bribe users to use it.
Therefore, for any deployed or undeployed dapp, the only way to prevent this attack is to modify the contract or deploy the proxy itself to return most of the revenue to users.
That's not enough. Eventually users will find that they don't need to care if the dapp they interact with registered for the CSR and returns all revenue. Because they can get all CSR fee by making all their transactions through their own proxy contracts.
Yes, the proxy is just like a smart wallet, and all users will love using a smart wallet.
However, this does not meet the design purpose of the canto CSR:
We present a novel economic mechanism which modifies EIP-1559 to distribute a portion of the total base fee (an amount that would otherwise be burnt) to the deployers of the contracts that consume gas within a given block.
The Canto CSR Store is a revenue-sharing-per-transaction model that allows smart contract developers to accumulate revenue to a tradable NFT.
VS Code
Real contract developers should be protected from CSR fee theft.
When sending CSR fee, not the entire CSR fee should be sent directly to the top contract(the to of the tx), but to every contract that is invoked.
This can be calculated and sent separately by taking the evm's call stack and gas consumption per contract call(internal transaction).
#0 - c4-judge
2022-11-30T09:35:29Z
berndartmueller marked the issue as duplicate of #48
#1 - c4-judge
2023-01-02T12:07:06Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: hihen
13609.3591 CANTO - $2,197.91
https://github.com/code-423n4/2022-11-canto/blob/2733fdd1bee73a6871c6243f92a007a0b80e4c61/Canto/x/csr/keeper/evm_hooks.go#L49 https://github.com/code-423n4/2022-11-canto/blob/2733fdd1bee73a6871c6243f92a007a0b80e4c61/Canto/x/csr/keeper/evm_hooks.go#L101-L135
Some contracts and some Turnstile tokens (nfts) wll not be able to receive CSR fees forever.
In evm_hooks.go, the PostTxProcessing will call h.processEvents(ctx, receipt)
to handle Register
and Assign
events from Turnstile contract first:
h.processEvents(ctx, receipt)
Notice that the processEvents
function does not return any error.
However, it is possible for processEvents to encounter an error:
func (h Hooks) processEvents(ctx sdk.Context, receipt *ethtypes.Receipt) { ... for _, log := range receipt.Logs { ... if log.Address == turnstileAddress { ... switch event.Name { case types.TurnstileEventRegister: err = h.k.RegisterEvent(ctx, log.Data) case types.TurnstileEventUpdate: err = h.k.UpdateEvent(ctx, log.Data) } if err != nil { h.k.Logger(ctx).Error(err.Error()) return } } } }
According to the above implementation of processEvents
, it will process all the events emitted by the transaction one by one. If one of them encounters an error, it will return directly without any error, and any subsequent unprocessed events will be ignored.
Suppose we have a transaction contains the following events(by contract calls):
If RegisterEvent()
returns an error when handling the first event, then all of the events will not be handled because processEvents()
will return after logging the error.
And PostTxProcessing()
continues to execute normally because it is unaware of the error.
According to the current implementation of RegisterEvent()
and UpdateEvent
, they are both easy to encounter an error. Like register()
using a recipient that doesn't exist yet.
As a result, none of the C1, C2, C3 contracts will be able to recieve any CSR fee because they are not recorded in csr store.
Contrcts C1, C2, C3 will never be able to register for CSR because they are marked registered in Turnstile contract (evm store) and will be reverted by onlyUnregistered
when calling register()
or assign()
.
And all other contracts calling assign(token1)
or assign(token2)
will enter the same state as C1/C2/C3, because the assign()
will succeed in Turnstile contract but fail in UpdateEvent()
(because the store can not find token1 or token2):
// Check if the NFT that is being updated exists in the CSR store nftID := event.TokenId.Uint64() csr, found := k.GetCSR(ctx, nftID) if !found { return sdkerrors.Wrapf(ErrNFTNotFound, "EventHandler::UpdateEvent the nft entered does not currently exist: %d", nftID) }
VS Code
processEvents()
should return the error it encounters, and PostTxProcessing()
should return that error too.
#0 - c4-sponsor
2022-12-21T04:29:54Z
tkkwon1998 marked the issue as sponsor confirmed
#1 - tkkwon1998
2022-12-21T04:37:03Z
If there are multiple TXs registering contracts, but one of them fails, all of them will fail without much of a warning, so issue confirmed. Although, instead of returning the error to revert the TX, the error should be logged and the for loop inside of processEvents should be allowed to continue.
#2 - c4-judge
2023-01-02T12:47:27Z
berndartmueller marked the issue as selected for report
4710.932 CANTO - $760.82
Transactions may fail because of this redundant check.
The function distributeFees will revert if msg.value == 0
:
function distributeFees(uint256 _tokenId) public onlyOwner payable { if (msg.value == 0) revert NothingToDistribute(); balances[_tokenId] += msg.value; emit DistributeFees(_tokenId, msg.value); }
According to evm_hooks.go - PostTxProcessing():
fee := sdk.NewIntFromUint64(receipt.GasUsed).Mul(sdk.NewIntFromBigInt(msg.GasPrice())) csrFee := sdk.NewDecFromInt(fee).Mul(params.CsrShares).TruncateInt() evmDenom := h.k.evmKeeper.GetParams(ctx).EvmDenom csrFees := sdk.Coins{{Denom: evmDenom, Amount: csrFee}}
it's possible for msg.value == 0
when calling distributeFees()
from evm_hooks.go:
CsrShares
is set to 0 for some reasonWhen that happened, the distributeFees() will revert, causing PostTxProcessing() to return with an error, causing the whole evm transaction being reverted.
VS Code
Should delete Turnstile.sol#L149.
#0 - c4-sponsor
2022-12-18T15:07:53Z
tkkwon1998 marked the issue as sponsor confirmed
#1 - tkkwon1998
2022-12-18T15:10:03Z
I think this is a duplicate of issue #94 , which covers the broader issue at hand: that returning errors in the PostTxProcessingHook will revert entire transactions.
The msg.value == 0 issue is just one example, which was mentioned by the warden that submitted issue 94.
#2 - c4-sponsor
2022-12-21T04:49:13Z
tkkwon1998 requested judge review
#3 - c4-judge
2023-01-02T12:40:59Z
berndartmueller marked the issue as duplicate of #94
#4 - c4-judge
2023-01-02T12:41:05Z
berndartmueller marked the issue as satisfactory