Secureum Bootcamp Epoch∞ - February RACE #5
This is a write-up of the Secureum Bootcamp Race 5 Quiz of Epoch Infinity (opens in a new tab) with solutions.
This quiz had a strict time limit of 16 minutes for 8 questions, no pause. Choose all and only correct answers.
Syntax highlighting was omitted since the original quiz did not have any either.
March 8, 2022 by patrickd
Code
All 8 questions in this quiz are based on the InSecureum
contract. This is the same contract you will see for all the 8 questions in this quiz. InSecureum
is adapted from a well-known contract.
pragma solidity ^0.8.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/IERC1155.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155//IERC1155Receiver.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155//extensions/IERC1155MetadataURI.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Context.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/introspection/ERC165.sol";
contract InSecureum is Context, ERC165, IERC1155, IERC1155MetadataURI {
mapping(uint256 => mapping(address => uint256)) private _balances;
mapping(address => mapping(address => bool)) private _operatorApprovals;
string private _uri;
constructor(string memory uri_) {
_setURI(uri_);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return
interfaceId == type(IERC1155).interfaceId ||
interfaceId == type(IERC1155MetadataURI).interfaceId ||
super.supportsInterface(interfaceId);
}
function uri(uint256) public view virtual override returns (string memory) {
return _uri;
}
function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
require(account != address(0), "ERC1155: balance query for the zero address");
return _balances[id][account];
}
function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
public
view
virtual
override
returns (uint256[] memory)
{
uint256[] memory batchBalances = new uint256[](accounts.length);
for (uint256 i = 0; i < accounts.length; ++i) {
batchBalances[i] = balanceOf(accounts[i], ids[i]);
}
return batchBalances;
}
function setApprovalForAll(address operator, bool approved) public virtual override {
_setApprovalForAll(_msgSender(), operator, approved);
}
function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
return _operatorApprovals[account][operator];
}
function safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public virtual override {
require(
from == _msgSender() || isApprovedForAll(from, _msgSender()),
"ERC1155: caller is not owner nor approved"
);
_safeTransferFrom(from, to, id, amount, data);
}
function safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) public virtual override {
require(
from == _msgSender() || isApprovedForAll(from, _msgSender()),
"ERC1155: transfer caller is not owner nor approved"
);
_safeBatchTransferFrom(from, to, ids, amounts, data);
}
function _safeTransferFrom(
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) public virtual {
address operator = _msgSender();
uint256 fromBalance = _balances[id][from];
unchecked {
fromBalance = fromBalance - amount;
}
_balances[id][from] = fromBalance;
_balances[id][to] += amount;
emit TransferSingle(operator, from, to, id, amount);
_doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}
function _safeBatchTransferFrom(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: transfer to the zero address");
address operator = _msgSender();
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 amount = amounts[i];
uint256 fromBalance = _balances[id][from];
fromBalance = fromBalance - amount;
_balances[id][to] += amount;
}
emit TransferBatch(operator, from, to, ids, amounts);
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}
function _setURI(string memory newuri) internal virtual {
_uri = newuri;
}
function _mint(
address to,
uint256 id,
uint256 amount,
bytes memory data
) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");
address operator = _msgSender();
_balances[id][to] += amount;
emit TransferSingle(operator, address(0), to, id, amount);
_doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}
function _mintBatch(
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual {
address operator = _msgSender();
require(operator != address(0), "ERC1155: mint from the zero address");
for (uint256 i = 0; i < ids.length; i++) {
_balances[ids[i]][to] += amounts[i];
}
emit TransferBatch(operator, address(0), to, amounts, ids);
_doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
}
function _burn(
address from,
uint256 id,
uint256 amount
) internal virtual {
require(from != address(0), "ERC1155: burn from the zero address");
address operator = _msgSender();
uint256 fromBalance = _balances[id][from];
_balances[id][from] = fromBalance - amount;
emit TransferSingle(operator, from, address(0), id, amount);
}
function _burnBatch(
address from,
uint256[] memory ids,
uint256[] memory amounts
) internal virtual {
require(from != address(0), "ERC1155: burn from the zero address");
address operator = _msgSender();
for (uint256 i = 0; i < ids.length; i++) {
uint256 id = ids[i];
uint256 amount = amounts[i];
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
unchecked {
_balances[id][from] = fromBalance - amount;
}
}
emit TransferBatch(operator, from, address(0), ids, amounts);
}
function _setApprovalForAll(
address owner,
address operator,
bool approved
) internal virtual {
require(owner != operator, "ERC1155: setting approval status for self");
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 amount,
bytes memory data
) private {
if (isContract(to)) {
try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
if (response == IERC1155Receiver.onERC1155Received.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non ERC1155Receiver implementer");
}
}
}
function _doSafeBatchTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) private {
if (isContract(to)) {
try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (
bytes4 response
) {
if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
revert("ERC1155: ERC1155Receiver rejected tokens");
}
} catch Error(string memory reason) {
revert(reason);
} catch {
revert("ERC1155: transfer to non ERC1155Receiver implementer");
}
}
}
function isContract(address account) internal view returns (bool) {
return account.code.length == 0;
}
}
Question 1 of 8
InSecureum balanceOf()
- A. May be optimised by caching state variable in local variable
- B. May be optimised by changing state mutability from
view
topure
- C. May be optimised by changing its visibility to
external
- D. None of the above
Solution
Correct is D.
Since the _balances
state variable is only accessed once and immediately returned, caching doesn't make sense.
State mutability can't be changed to pure
since the function accesses a state variable, that requires at least view
.
It can't be changed to external
because it is currently being called internally by the balanceOfBatch()
function.
Question 2 of 8
In InSecureum
, array lengths mismatch check is missing in
- A.
balanceOfBatch()
- B.
_safeBatchTransferFrom()
- C.
_mintBatch()
- D.
_burnBatch()
Solution
Correct is A, B, C, D.
The public function balanceOfBatch()
receives a list of accounts
and a list of ids
, both of which items get passed on to balanceOf(accounts[i], ids[i]);
. To ensure that neither array is accessed out-of-bounds, it should be checked whether both lists are of the same length.
Neither the internal function _safeBatchTransferFrom()
nor its public caller function safeBatchTransferFrom()
check the length of passed ids
and amounts
. Therefore the check is missing.
The internal functions _mintBatch()
and _burnBatch()
are currently never called, but a contract extending InSecureum might. It would make sense to check the lengths of passed ids
and amounts
in them, so that public functions calling them do not need to remember to do so.
Question 3 of 8
The security concern(s) with InSecureum _safeTransferFrom()
is/are
- A. Incorrect visibility
- B. Susceptible to an integer underflow
- C. Missing zero-address validation
- D. None of the above
Solution
Correct is A, B, C.
It is prefixed with an underscore, which is usually an indication of an internal
visibility, and it's also called by a similarly named public safeTransferFrom()
function that applies more input validation before calling it. This validation ensures that the sender actually has approval for the transfer of funds, which would be bypassed by this function being public. It should instead be internal
allowing an inheriting contract to internally call it.
The new fromBalance
is calculated within an unchecked{}
block, bypassing integer underflow prevention measures of Solidity version 0.8.0^
. Since the fromBalance
isn't checked for whether there's a sufficient balance for a transfer, this effectively allows sending unlimited amounts to the specified recipient.
Neither safeTransferFrom()
nor _safeTransferFrom()
are checking whether the to
address is non-zero, making it possible to accidentally burn tokens.
Question 4 of 8
The security concern(s) with InSecureum _safeBatchTransferFrom()
is/are
- A. Missing array lengths mismatch check
- B. Susceptibility to an integer underflow
- C. Incorrect balance update
- D. None of the above
Solution
Correct is A, C.
The fact that the array lengths mismatch check is missing has already been determined in Question #2.
There's no usage of an unchecked{}
block, therefore an integer underflow cannot happen with this Solidity version.
The new value of fromBalance
is calculated but it's never actually updated in storage. This effectively allows sending the same tokens unlimited amount of times.
Question 5 of 8
The security concern(s) with InSecureum _mintBatch()
is/are
- A. Missing array lengths mismatch check
- B. Incorrect event emission
- C. Allows burning of tokens
- D. None of the above
Solution
Correct is A, B, C.
The fact that the array lengths mismatch check is missing has already been determined in Question #2.
Comparing the emission of the TransferBatch
event to other occurrences, it appears that ids
and amounts
have been accidentally swapped.
The zero-address check incorrectly ensures that the sender is non-zero (which would never be possible anyway) instead of ensuring that the receiving account is non-zero. This effectively allows minting to the zero-address, burning all minted tokens immediately.
Question 6 of 8
The security concern(s) with InSecureum _burn()
is/are
- A. Missing zero-address validation
- B. Susceptibility to an integer underflow
- C. Incorrect balance update
- D. None of the above
Solution
Correct is D.
The zero-address validation exists and is correctly checking the value of from
.
There's no usage of an unchecked{}
block, therefore an integer underflow cannot happen with this Solidity version.
The balance appears to be correctly updated after subtraction.
Question 7 of 8
The security concern(s) with InSecureum _doSafeTransferAcceptanceCheck()
is/are
- A.
isContract
check on incorrect address - B. Incorrect check on return value
- C. Call to incorrect
isContract
implementation - D. None of the above
Solution
Correct is B, C.
The isContract()
function is correctly called on to
, which is the receiving address that is potentially a contract that this function is supposed to check support of ERC1155, before tokens are sent to it, since they'd otherwise be stuck in a contract not supporting this standard.
Comparing _doSafeTransferAcceptanceCheck()
and _doSafeBatchTransferAcceptanceCheck()
shows a clear discrepancy when checking the return value, with the batch function's implementation correctly checking support for the ERC1155 standard. This function is in fact currently doing the opposite, ensuring that tokens are only sent to contracts that do NOT support it.
The isContract()
function currently returns true
if the passed address is in fact NOT a contract (has a code length of 0). It should instead return true only when the address has a code length larger than 0, showing that there's currently a contract residing at account
.
Question 8 of 8
The security concern(s) with InSecureum isContract()
implementation is/are
- A. Incorrect visibility
- B. Incorrect operator in the comparison
- C. Unnecessary because Ethereum only has Contract accounts
- D. None of the above
Solution
Correct is B.
A visibility of internal
allowing inheriting contracts to use it appears appropriate.
The comparison should indeed be "bigger-than-zero" instead of "equals-zero", for the reasons explained for the previous question.
Ethereum not only has Contract accounts but also EOA (Externally Owned Accounts), which do not have any contract code but an off-chain public-private keypair instead.