Alchemy Toys Arena - Smart Contract Audit Report
Summary
Alchemy Toys is building a new component to their Alchemy Toys game where users can stake collections of toys called Hands and challenge other Hands to battle in the Arena.
For this audit, we reviewed the project's ArenaMaster, Hand, XP, and Arena contracts at commit 83f1e769dd7eae03df418c7dcd497ac8e997eae4 on the team's private GitLab repo.
We previously reviewed the project team's Alchemy Toys project here.
Notes on Individual Contracts:
ArenaMaster contract:Hand contract:
- The ArenaMaster contract is the controller which keeps track of the active Arenas.
- The owner has the Default Admin role.
- The owner is able to grant other wallet or contract addresses the Default Admin role.
- Only users with the Default Admin role may add or remove an address as an Arena.
XP contract:
- The Hand contract allows users to use their Toys to create a Hand to participate in the Arena.
- The user must have exactly the required amount of Toys to create a Hand; the Enlightenment toy is excluded and cannot be used in a Hand.
- When creating a Hand using the mint() function, the Toys must be entered in ascending order by strength; otherwise, the entire function call will fail.
- A Hand is minted to the user, and the Toys are transferred from the user to the contract address.
- The user is able to transfer all the Toys in the Hand to any address; the Hand is subsequently burned.
- Only an active Arena with the proper approvals for a Hand is able to set the stage strength for a Hand, and change the power value for a Hand.
Arena contract:
- The XP contract is an ERC20 token used to reward users in the Arena.
- The total supply of the XP token is initially minted to the owner.
- The owner has the Default Admin role.
- The owner is able to grant other wallet or contract addresses the Default Admin role.
- Users with the Default Admin role can mint any amount of the XP token to any address.
General notes across all contracts:
- The owner has the Default Admin role.
- The owner is able to grant other wallet or contract addresses the Default Admin role.
- Users with the Default Admin role are able to set the values of various variables used in the Arena; these values are uncapped and can be changed at any time.
- The Admins must be careful to not set an affiliate fee percent that is greater than the total fee percent, as they will not be able to use the setupLoot() function to change these values once this mistake is made.
- Anyone is able to stake their Hand in the Arena and set an affiliate address, who will receive a portion of the entry fees charged; the affiliate can only be set the first time a user participates in staking.
- When a user stakes their Hand, the Hand is transferred to the contract address.
- An entry fee is collected upon staking; a portion of the fees goes to the affiliate address (if any), another portion is sent to the burn address, and the remaining part is transferred to the contract as the loot amount.
- Users are granted XP tokens as a power up based on the Toys in their Hand; users holding the Pocket Rocket toy qualify for an additional 10% power up.
- The power up is also used to adjust the stage strength of the Hand; the average strength of the arena is subsequently updated based on the user's calculated stage strength.
- An Arena token representing the stake is minted to the user.
- The user can use their Arena token to challenge other Hands staked in the Arena; the challenged hand (the defender) is chosen on a pseudo-random basis.
- The randomness function used to determine the defender relies on some predictable variables that can be manipulated by miners to some extent. This is common, albeit not best practice, but the probability of miners maliciously changing these variables is extremely low.
- The winner is ultimately decided based on the power of each Hand; if the defender is holding the Dragonslayer toy, the defender will receive an additional 10% power bonus.
- In the event that the defender's power exceeds the contender's power by 50%, and the contender's Hand includes the Oldschool GPS toy, the contender will automatically retreat from the challenge, keeping his Hand intact.
- Otherwise, the player with the greater power is the winner.
- As a reward, each player will receive 20% of the opponent's stage strength value in XP tokens; if the winner's Hand includes the GoodhoodToken toy, the winner will receive an additional 20% on top of the reward XP value.
- The stage strength of the winner's Hand is decreased by a percentage of the Hand's current strength amount above the base strength.
- The power of the winner's Hand is also decreased by a value equivalent to half of the difference between the winner's stage strength and the loser's stage strength.
- The winner receives the loser's deposited loot amount; if the loser's Hand includes the GATlantis toy, the loser will receive 20% of the loot amount back as a refund.
- The loser's staking rewards are transferred to the user in XP tokens or any other ERC20 token the project team has set as the reward token.
- The team must exercise caution when setting the reward token and must avoid using ERC777-compliant tokens (this is uncommon).
- The staking reward is calculated as the amount of time staked multiplied by the rewards rate, which is set by the team and can change at any time.
- Users with the AlchemyArena toy in their hand will receive 2x rewards.
- The loser must fold and his Hand will be delivered to the winner.
- The weakest toy in the Hand will be burned, unless either the winner has the William Snakespeare toy or the loser has the Artificial Heart toy.
- The loser will be able to keep his weakest toy if the Hand includes the Artificial Heart toy; otherwise, the weakest toy will go to the winner if the Hand includes the William Snakespeare toy.
- The loser's Arena token is subsequently burned and the challenge is finished.
- Users who have an Arena token are able to unstake their Hand at any time.
- Users whose Hand includes the Golden Dragon toy will receive 25% of the deposited loot amount back as a refund; any remaining loot amount will be burned.
- Users can harvest their staking rewards at any time.
- The Admins can withdraw any reward tokens in the contract balance at any time.
- The Admins can set the entry fee to any value at any time.
- Excellent structuring of logic to prevent loops from hitting the block gas limit, and to prevent reentrancy attacks.
- As the contract is implemented with Solidity 0.8.x, it is protected from overflows.
- The contracts comply with the ERC-721 standard.
Audit Findings Summary:
- No security issues from outside attackers were identified.
- Ensure trust in the team as they have substantial control in the ecosystem.
- Date: August 19th, 2021.
External Threat Results
Vulnerability Category | Notes | Result |
---|---|---|
Arbitrary Storage Write | N/A | PASS |
Arbitrary Jump | N/A | PASS |
Delegate Call to Untrusted Contract | N/A | PASS |
Dependence on Predictable Variables | Decisions are made based on the block.timestamp, block.number, block.difficulty, etc environment variables which can be manipulated by a malicious miner. This is extremley unlikely to occur. | WARNING |
Deprecated Opcodes | N/A | PASS |
Ether Thief | N/A | PASS |
Exceptions | N/A | PASS |
External Calls | N/A | PASS |
Integer Over/Underflow | N/A | PASS |
Multiple Sends | N/A | PASS |
Suicide | N/A | PASS |
State Change External Calls | N/A | Pass |
Unchecked Retval | N/A | PASS |
User Supplied Assertion | N/A | PASS |
Critical Solidity Compiler | N/A | PASS |
Overall Contract Safety | PASS |
Arena Master Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ Initializable
+ ContextUpgradeable (Initializable)
- [Int] __Context_init #
- modifiers: initializer
- [Int] __Context_init_unchained #
- modifiers: initializer
- [Int] _msgSender
- [Int] _msgData
+ [Lib] StringsUpgradeable
- [Int] toString
- [Int] toHexString
- [Int] toHexString
+ [Int] IERC165Upgradeable
- [Ext] supportsInterface
+ ERC165Upgradeable (Initializable, IERC165Upgradeable)
- [Int] __ERC165_init #
- modifiers: initializer
- [Int] __ERC165_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
+ [Int] IAccessControlUpgradeable
- [Ext] hasRole
- [Ext] getRoleAdmin
- [Ext] grantRole #
- [Ext] revokeRole #
- [Ext] renounceRole #
+ AccessControlUpgradeable (Initializable, ContextUpgradeable, IAccessControlUpgradeable, ERC165Upgradeable)
- [Int] __AccessControl_init #
- modifiers: initializer
- [Int] __AccessControl_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
- [Pub] hasRole
- [Int] _checkRole
- [Pub] getRoleAdmin
- [Pub] grantRole #
- modifiers: onlyRole
- [Pub] revokeRole #
- modifiers: onlyRole
- [Pub] renounceRole #
- [Int] _setupRole #
- [Int] _setRoleAdmin #
- [Prv] _grantRole #
- [Prv] _revokeRole #
+ ArenaMaster (AccessControlUpgradeable)
- [Pub] initialize #
- modifiers: initializer
- [Int] __ArenaMaster_init_unchained #
- modifiers: initializer
- [Ext] setArena #
- modifiers: onlyRole
Arena Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ Initializable
+ ContextUpgradeable (Initializable)
- [Int] __Context_init #
- modifiers: initializer
- [Int] __Context_init_unchained #
- modifiers: initializer
- [Int] _msgSender
- [Int] _msgData
+ [Lib] StringsUpgradeable
- [Int] toString
- [Int] toHexString
- [Int] toHexString
+ [Int] IERC165Upgradeable
- [Ext] supportsInterface
+ ERC165Upgradeable (Initializable, IERC165Upgradeable)
- [Int] __ERC165_init #
- modifiers: initializer
- [Int] __ERC165_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
+ [Int] IAccessControlUpgradeable
- [Ext] hasRole
- [Ext] getRoleAdmin
- [Ext] grantRole #
- [Ext] revokeRole #
- [Ext] renounceRole #
+ AccessControlUpgradeable (Initializable, ContextUpgradeable, IAccessControlUpgradeable, ERC165Upgradeable)
- [Int] __AccessControl_init #
- modifiers: initializer
- [Int] __AccessControl_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
- [Pub] hasRole
- [Int] _checkRole
- [Pub] getRoleAdmin
- [Pub] grantRole #
- modifiers: onlyRole
- [Pub] revokeRole #
- modifiers: onlyRole
- [Pub] renounceRole #
- [Int] _setupRole #
- [Int] _setRoleAdmin #
- [Prv] _grantRole #
- [Prv] _revokeRole #
+ [Lib] ERC165CheckerUpgradeable
- [Int] supportsERC165
- [Int] supportsInterface
- [Int] getSupportedInterfaces
- [Int] supportsAllInterfaces
- [Prv] _supportsERC165Interface
+ [Int] IERC20
- [Ext] totalSupply
- [Ext] balanceOf
- [Ext] transfer #
- [Ext] allowance
- [Ext] approve #
- [Ext] transferFrom #
+ [Int] IERC721Upgradeable (IERC165Upgradeable)
- [Ext] balanceOf
- [Ext] ownerOf
- [Ext] safeTransferFrom #
- [Ext] transferFrom #
- [Ext] approve #
- [Ext] getApproved
- [Ext] setApprovalForAll #
- [Ext] isApprovedForAll
- [Ext] safeTransferFrom #
+ [Int] IERC721ReceiverUpgradeable
- [Ext] onERC721Received #
+ [Int] IERC721MetadataUpgradeable (IERC721Upgradeable)
- [Ext] name
- [Ext] symbol
- [Ext] tokenURI
+ [Lib] AddressUpgradeable
- [Int] isContract
- [Int] sendValue #
- [Int] functionCall #
- [Int] functionCall #
- [Int] functionCallWithValue #
- [Int] functionCallWithValue #
- [Int] functionStaticCall
- [Int] functionStaticCall
- [Prv] _verifyCallResult
+ ERC721Upgradeable (Initializable, ContextUpgradeable, ERC165Upgradeable, IERC721Upgradeable, IERC721MetadataUpgradeable)
- [Int] __ERC721_init #
- modifiers: initializer
- [Int] __ERC721_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
- [Pub] balanceOf
- [Pub] ownerOf
- [Pub] name
- [Pub] symbol
- [Pub] tokenURI
- [Int] _baseURI
- [Pub] approve #
- [Pub] getApproved
- [Pub] setApprovalForAll #
- [Pub] isApprovedForAll
- [Pub] transferFrom #
- [Pub] safeTransferFrom #
- [Pub] safeTransferFrom #
- [Int] _safeTransfer #
- [Int] _exists
- [Int] _isApprovedOrOwner
- [Int] _safeMint #
- [Int] _safeMint #
- [Int] _mint #
- [Int] _burn #
- [Int] _transfer #
- [Int] _approve #
- [Prv] _checkOnERC721Received #
- [Int] _beforeTokenTransfer #
+ [Int] IERC721EnumerableUpgradeable (IERC721Upgradeable)
- [Ext] totalSupply
- [Ext] tokenOfOwnerByIndex
- [Ext] tokenByIndex
+ ERC721EnumerableUpgradeable (ERC721Upgradeable, IERC721EnumerableUpgradeable)
- [Pub] supportsInterface
- [Pub] tokenOfOwnerByIndex
- [Pub] totalSupply
- [Pub] tokenByIndex
- [Int] _beforeTokenTransfer #
- [Prv] _addTokenToOwnerEnumeration #
- [Prv] _addTokenToAllTokensEnumeration #
- [Prv] _removeTokenFromOwnerEnumeration #
- [Prv] _removeTokenFromAllTokensEnumeration #
+ [Int] IRewardable (IERC20)
- [Ext] reward #
+ [Int] IArena (IERC721EnumerableUpgradeable)
+ [Int] IHand (IERC721EnumerableUpgradeable)
- [Ext] getInfo
- [Ext] getToysInfo
- [Ext] getToysInfo
- [Ext] power
- [Ext] setStageStrength #
- [Ext] fold #
- [Ext] changePower #
- [Ext] hasToy
- [Ext] hasToy
+ Arena (IArena, AccessControlUpgradeable, ERC721EnumerableUpgradeable)
- [Ext] initialize #
- modifiers: initializer
- [Int] __Arena_init_unchained #
- modifiers: initializer
- [Ext] setupLoot #
- modifiers: onlyRole
- [Ext] setupSlashing #
- modifiers: onlyRole
- [Ext] setupXP #
- modifiers: onlyRole
- [Ext] setupRewards #
- modifiers: onlyRole
- [Ext] setupTiming #
- modifiers: onlyRole
- [Ext] stake #
- [Ext] unstake #
- [Ext] challenge #
- modifiers: onlyActive
- [Ext] harvestRewards #
- [Prv] harvestRewards #
- [Pub] getInfo
- [Pub] getHandChallengeStrength
- [Ext] getMaxPowerUp
- [Pub] getMaxPowerUp
- [Pub] getFeeBreakDown
- [Pub] getEntryFee
- [Ext] unstakeRewards #
- modifiers: onlyRole
- [Ext] setEntryFee #
- modifiers: onlyRole
- [Ext] setBaseURI #
- modifiers: onlyRole
- [Pub] supportsInterface
- [Int] _transferXP #
- [Int] _finishChallenge #
- [Int] _selectDefender
- [Int] _addAverageStrength #
- [Int] _removeAverageStrength #
- [Int] _updateAverageStrength #
- [Int] _getRandomNumber
Hand Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ Initializable
+ ContextUpgradeable (Initializable)
- [Int] __Context_init #
- modifiers: initializer
- [Int] __Context_init_unchained #
- modifiers: initializer
- [Int] _msgSender
- [Int] _msgData
+ [Lib] StringsUpgradeable
- [Int] toString
- [Int] toHexString
- [Int] toHexString
+ [Int] IERC165Upgradeable
- [Ext] supportsInterface
+ ERC165Upgradeable (Initializable, IERC165Upgradeable)
- [Int] __ERC165_init #
- modifiers: initializer
- [Int] __ERC165_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
+ [Int] IAccessControlUpgradeable
- [Ext] hasRole
- [Ext] getRoleAdmin
- [Ext] grantRole #
- [Ext] revokeRole #
- [Ext] renounceRole #
+ AccessControlUpgradeable (Initializable, ContextUpgradeable, IAccessControlUpgradeable, ERC165Upgradeable)
- [Int] __AccessControl_init #
- modifiers: initializer
- [Int] __AccessControl_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
- [Pub] hasRole
- [Int] _checkRole
- [Pub] getRoleAdmin
- [Pub] grantRole #
- modifiers: onlyRole
- [Pub] revokeRole #
- modifiers: onlyRole
- [Pub] renounceRole #
- [Int] _setupRole #
- [Int] _setRoleAdmin #
- [Prv] _grantRole #
- [Prv] _revokeRole #
+ [Int] IERC721Upgradeable (IERC165Upgradeable)
- [Ext] balanceOf
- [Ext] ownerOf
- [Ext] safeTransferFrom #
- [Ext] transferFrom #
- [Ext] approve #
- [Ext] getApproved
- [Ext] setApprovalForAll #
- [Ext] isApprovedForAll
- [Ext] safeTransferFrom #
+ [Int] IERC721ReceiverUpgradeable
- [Ext] onERC721Received #
+ [Int] IERC721MetadataUpgradeable (IERC721Upgradeable)
- [Ext] name
- [Ext] symbol
- [Ext] tokenURI
+ [Lib] AddressUpgradeable
- [Int] isContract
- [Int] sendValue #
- [Int] functionCall #
- [Int] functionCall #
- [Int] functionCallWithValue #
- [Int] functionCallWithValue #
- [Int] functionStaticCall
- [Int] functionStaticCall
- [Prv] _verifyCallResult
+ ERC721Upgradeable (Initializable, ContextUpgradeable, ERC165Upgradeable, IERC721Upgradeable, IERC721MetadataUpgradeable)
- [Int] __ERC721_init #
- modifiers: initializer
- [Int] __ERC721_init_unchained #
- modifiers: initializer
- [Pub] supportsInterface
- [Pub] balanceOf
- [Pub] ownerOf
- [Pub] name
- [Pub] symbol
- [Pub] tokenURI
- [Int] _baseURI
- [Pub] approve #
- [Pub] getApproved
- [Pub] setApprovalForAll #
- [Pub] isApprovedForAll
- [Pub] transferFrom #
- [Pub] safeTransferFrom #
- [Pub] safeTransferFrom #
- [Int] _safeTransfer #
- [Int] _exists
- [Int] _isApprovedOrOwner
- [Int] _safeMint #
- [Int] _safeMint #
- [Int] _mint #
- [Int] _burn #
- [Int] _transfer #
- [Int] _approve #
- [Prv] _checkOnERC721Received #
- [Int] _beforeTokenTransfer #
+ [Int] IERC721EnumerableUpgradeable (IERC721Upgradeable)
- [Ext] totalSupply
- [Ext] tokenOfOwnerByIndex
- [Ext] tokenByIndex
+ ERC721EnumerableUpgradeable (ERC721Upgradeable, IERC721EnumerableUpgradeable)
- [Pub] supportsInterface
- [Pub] tokenOfOwnerByIndex
- [Pub] totalSupply
- [Pub] tokenByIndex
- [Int] _beforeTokenTransfer #
- [Prv] _addTokenToOwnerEnumeration #
- [Prv] _addTokenToAllTokensEnumeration #
- [Prv] _removeTokenFromOwnerEnumeration #
- [Prv] _removeTokenFromAllTokensEnumeration #
+ [Int] IHand (IERC721EnumerableUpgradeable)
- [Ext] getInfo
- [Ext] getToysInfo
- [Ext] getToysInfo
- [Ext] power
- [Ext] setStageStrength #
- [Ext] fold #
- [Ext] changePower #
- [Ext] hasToy
- [Ext] hasToy
+ [Int] IERC165
- [Ext] supportsInterface
+ [Int] IERC721 (IERC165)
- [Ext] balanceOf
- [Ext] ownerOf
- [Ext] safeTransferFrom #
- [Ext] transferFrom #
- [Ext] approve #
- [Ext] getApproved
- [Ext] setApprovalForAll #
- [Ext] isApprovedForAll
- [Ext] safeTransferFrom #
+ [Int] IERC721Metadata (IERC721)
- [Ext] name
- [Ext] symbol
- [Ext] tokenURI
+ [Int] IERC721Enumerable (IERC721)
- [Ext] totalSupply
- [Ext] tokenOfOwnerByIndex
- [Ext] tokenByIndex
+ [Int] IAlchemyToysToken (IERC721, IERC721Metadata, IERC721Enumerable)
- [Ext] contractURI
- [Ext] getLevelsConfig
- [Ext] tokenInfo
- [Ext] tokenPairID
- [Ext] isSpecialToken
- [Ext] getCurrentNumber
- [Ext] tokenOfOwnerByIndexRange
- [Ext] give #
- [Ext] giveSpecial #
- [Ext] giveAll #
- [Ext] burn #
+ ArenaMaster (AccessControlUpgradeable)
- [Pub] initialize #
- modifiers: initializer
- [Int] __ArenaMaster_init_unchained #
- modifiers: initializer
- [Ext] setArena #
- modifiers: onlyRole
+ Hand (IHand, AccessControlUpgradeable, ERC721EnumerableUpgradeable)
- [Pub] initialize #
- modifiers: initializer
- [Int] __Hand_init_unchained #
- modifiers: initializer
- [Ext] mint #
- [Pub] getInfo
- [Ext] getToysInfo
- [Pub] getToysInfo
- [Ext] getBaseStrengths
- [Ext] getBaseStrength
- [Pub] getBaseStrength
- [Ext] canUseToy
- [Pub] canUseToy
- [Ext] fold #
- [Ext] setStageStrength #
- modifiers: onlyArenas
- [Ext] changePower #
- modifiers: onlyArenas
- [Ext] power
- [Ext] hasToy
- [Pub] hasToy
- [Ext] setBaseURI #
- modifiers: onlyRole
- [Pub] supportsInterface
- [Int] SQRT
XP Contract
($) = payable function
# = non-constant function
Int = Internal
Ext = External
Pub = Public
+ [Int] IERC20
- [Ext] totalSupply
- [Ext] balanceOf
- [Ext] transfer #
- [Ext] allowance
- [Ext] approve #
- [Ext] transferFrom #
+ [Int] IERC20Metadata (IERC20)
- [Ext] name
- [Ext] symbol
- [Ext] decimals
+ Context
- [Int] _msgSender
- [Int] _msgData
+ ERC20 (Context, IERC20, IERC20Metadata)
- [Pub] #
- [Pub] name
- [Pub] symbol
- [Pub] decimals
- [Pub] totalSupply
- [Pub] balanceOf
- [Pub] transfer #
- [Pub] allowance
- [Pub] approve #
- [Pub] transferFrom #
- [Pub] increaseAllowance #
- [Pub] decreaseAllowance #
- [Int] _transfer #
- [Int] _mint #
- [Int] _burn #
- [Int] _approve #
- [Int] _beforeTokenTransfer #
- [Int] _afterTokenTransfer #
+ [Lib] Strings
- [Int] toString
- [Int] toHexString
- [Int] toHexString
+ [Int] IERC165
- [Ext] supportsInterface
+ ERC165 (IERC165)
- [Pub] supportsInterface
+ [Int] IAccessControl
- [Ext] hasRole
- [Ext] getRoleAdmin
- [Ext] grantRole #
- [Ext] revokeRole #
- [Ext] renounceRole #
+ AccessControl (Context, IAccessControl, ERC165)
- [Pub] supportsInterface
- [Pub] hasRole
- [Int] _checkRole
- [Pub] getRoleAdmin
- [Pub] grantRole #
- modifiers: onlyRole
- [Pub] revokeRole #
- modifiers: onlyRole
- [Pub] renounceRole #
- [Int] _setupRole #
- [Int] _setRoleAdmin #
- [Prv] _grantRole #
- [Prv] _revokeRole #
+ [Int] IRewardable (IERC20)
- [Ext] reward #
+ XP (IRewardable, AccessControl, ERC20)
- [Pub] #
- modifiers: ERC20
- [Ext] reward #
- modifiers: onlyRole
- [Pub] supportsInterface