Platform: Code4rena
Start Date: 10/06/2021
Pot Size: $45,000 USDC
Total HM: 21
Participants: 12
Period: 7 days
Judge: LSDan
Total Solo HM: 13
Id: 13
League: ETH
Rank: 12/12
Findings: 2
Award: $259.51
🌟 Selected for report: 2
🚀 Solo Findings: 0
259.5075 USDC - $259.51
axic
Some major tokens went live before ERC20 was finalised, resulting in a discrepancy whether the transfer functions a) should return a boolean or b) revert/fail on error. The current best practice is that they should revert, but return “true” on success. However, not every token claiming ERC20-compatibility is doing this — some only return true/false; some revert, but do not return anything on success. This is a well known issue, heavily discussed since mid-2018.
Today many tools, including OpenZeppelin, offer a wrapper for “safe ERC20 transfer”: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol
RealityCards is not using such a wrapper, but instead tries to ensure successful transfers via the balancedBooks
modifier:
modifier balancedBooks { _; // using >= not == in case anyone sends tokens direct to contract require( erc20.balanceOf(address(this)) >= totalDeposits + marketBalance + totalMarketPots, "Books are unbalanced!" ); }
This modifier is present on most functions, but is missing on topupMarketBalance
:
function topupMarketBalance(uint256 _amount) external override { erc20.transferFrom(msgSender(), address(this), _amount); if (_amount > marketBalanceDiscrepancy) { marketBalanceDiscrepancy = 0; } else { marketBalanceDiscrepancy -= _amount; } marketBalance += _amount; }
In the case an ERC20 token which is not reverting on failures is used, a malicious actor could call topupMarketBalance
with a failing transfer, but also move the value of marketBalance
above the actual holdings. After this, deposit
, withdrawDeposit
, payRent
, payout
, sponsor
, etc. could be locked up and always failing with “Books are unbalanced”.
Anyone can call topupMarketBalance
with some unrealistically large number, so that marketBalance
does not overflow, but is above the actually helping balances. This is only possible if the underlying ERC20 used is not reverting on failures, but return “false” instead.
Manual review
balancedBooks
modifier#0 - Splidge
2021-06-14T08:56:23Z
The particular ERC20 contracts we are using don't have this issue. However for futureproofing in the event we change ERC20 tokens we will implement the recommended mitigation 1 and start using OpenZeppelin’s SafeERC20.
#1 - Splidge
2021-06-17T10:38:19Z
Fix implemented here
🌟 Selected for report: axic
0 USDC - $0.00
axic
In lib/NativeMetaTransactions.sol
there is a frequently used helper msgSender
:
function msgSender() internal view returns (address payable sender) { if (msg.sender == address(this)) { bytes memory array = msg.data; uint256 index = msg.data.length; assembly { // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. sender := and( mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff ) } } else { ...
Even though only the last 20-bytes matter, the bytes memory array = msg.data;
line causes the entire calldata to be copied to memory. This is exaggerated by the fact, that if msgSender()
is called multiple times in a transaction, the calldata will be also copied multiple times as memory is not freed.
N/A
Manual review.
There are multiple ways to avoid this:
Something along the lines of (untested!):
// Copy last 20 bytes bytes calldata data = msg.data[(msg.data.length - 20):]; sender = payable(address(uint160(bytes20(data))));
The OpenZeppelin implementation (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol#L21-L30) is an example of an optimised assembly version:
assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) }
One must note that the pure assembly version is obviously the most gas efficient, at least today.
#0 - Splidge
2021-06-17T10:36:39Z
Fix implemented here