A time-boxed security review of the Azuro protocol was done by pashov, with a focus on the security aspects of the application's implementation.
A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or if even the review will find any problems with your smart contracts.
Copied from the first security review
Azuro is a decentralized betting protocol. Anyone can launch a frontend service that connects to the smart contracts and to receive an affiliate bonus for each bet made through the given frontend. Different betting events can be hosted, for example a football game. Odds are provided once by a Data Feed provider (Oracle) for initialization and then odds change based on the betting on the platform. A user bet gets automatically converted to an NFT in the user's wallet.
| Severity | Impact: High | Impact: Medium | Impact: Low |
|---|---|---|---|
| Likelihood: High | Critical | High | Medium |
| Likelihood: Medium | High | Medium | Low |
| Likelihood: Low | Medium | Low | Low |
review commit hash - 1c475b43e47798ae0a49716fe949b523a2663d0a
The following smart contracts were in scope of the audit:
BetExpress
The following number of issues were found, categorized by their severity:
- Critical & High: 1 issues
- Medium: 2 issues
- Low: 1 issues
- Informational: 8 issues
| ID | Title | Severity |
|---|---|---|
| [C-01] | Value of leaf argument when calling addReserve is hardcoded incorrectly |
Critical |
| [M-01] | The protection check for maxBetShare can be gamed |
Medium |
| [M-02] | Tokens with a no-op fallback function can be used to steal the ETH balance of LP |
Medium |
| [L-01] | Using 0 as an argument value is error-prone | Low |
| [I-01] | Off-by-one error on timestamp check | Informational |
| [I-02] | The word "core" has multiple meanings in the protocol which raises complexity | Informational |
| [I-03] | Redundant getter | Informational |
| [I-04] | Missing event emission | Informational |
| [I-05] | Missing override keyword |
Informational |
| [I-06] | Unused imports | Informational |
| [I-07] | Incorrect comment | Informational |
| [I-08] | Use a safe pragma statement | Informational |
Impact: High, because liquidity won't be returned to the LiquidityTree
Likelihood: High, because the incorrect value is hardcoded and can't be changed
In BetExpress::resolvePayout we can see the following code:
uint128 reward = lp.addReserve(
0,
fullPayout - amount,
fullPayout - payout,
0
);where the last argument is 0 sent as a value for the leaf parameter. Since the leafs counting begins at 1, this will always be wrong and the liquidity won't be returned to the LiquidityTree.
The value of leaf should be the leaf value of each condition in the bet. The current design of resolvePayout does not allow to work on each condition in isolation, so this would need a redesign where you handle each condition separately.
Impact: Medium, because a protocol invariant can be broken and the code gives a false sense of security
Likelihood: Medium, as it can easily be gamed but there is no incentive for an attacker
The lockLiquidity method tries to block a single bet from taking up too much of the LP's allowed liquidity limit, but this can be gamed by splitting a very large bet into a big number of smaller ones, so this LargeBet custom error check would give a false sense of security as it doesn't guarantee what it intended to.
Change the validation to be based on all bets made through BetExpress instead of on each bet in isolation.
Impact: High, because it can lead to stolen funds from the protocol
Likelihood:
Low, as it requires a token with a fallback function but without a withdraw function
In LP::withdrawPayout we have the following code:
if (isNative) {
IWNative(token).withdraw(amount);
TransferHelper.safeTransferETH(account, amount);
} else {
TransferHelper.safeTransfer(token, account, amount);
}
Now imagine the following scenario:
- The
tokenused in the contract is one that does not have awithdrawfunction but has a fallback function - An attacker has a winning bet of 100 * 1e18 tokens
- Now he calls
withdrawPayoutbut sets theisNativeflag totrue - The
IWNative(token).withdraw(amount);will not revert but will be a no-op because of the fallback function oftoken - The attacker will receive 100 ETH instead of 100 * 1e18 tokens
The attack is similar to this one and even though it requires a special token and the LP to hold liquidity it is still a potential attack vector.
You can implement team processes about adding specific token contracts to be used in LP, where you have a checklist that contains not including tokens with a fallback function that are missing a withdraw function. You can also check the balance of LP before and after the withdraw call so you see it changed accordingly.
It is a best practice to overload methods so they have signatures that omit the arguments where 0 is a valid value. Intentionally using 0 as a valid value is error-prone and has lead to high severity issues in multiple protocols in the past.
The code in _conditionIsRunning reverts when block.timestamp >= startsAt but if block.timestamp == startsAt this should mean condition is running, so shouldn't result in a revert.
- block.timestamp >= startsAt
+ block.timestamp > startsAtThe word "core" is used both as a contract name (Core, CoreBase) as well as a word that means something that is a part of the protocol, for example BetExpress. This is non-intuitive and raises the complexity of the protocol which is non-ideal - consider using different wording for both meanings of "core" in the codebase.
_baseURI getter is redundant since there is already baseURI getter automatically generated.
The setBaseURI method in BetExpress.soldoes not emit an event which might not be good for off-chain monitoring. Emit an event on state change.
Methods initialize & viewPayout in BetExpress.sol are missing override keyword despite inheriting their function's signature from ICoreBased.sol & IBet.sol respectively.
import "./libraries/Math.sol" and "@uniswap/lib/contracts/libraries/TransferHelper.sol" are not used in BetExpress.sol and can be removed.
We have the following comment in putBet method in BetExpress.sol:
@notice Liquidity Pool: See {IBetEngine-putBet}.The right interface in this case is IBet.sol instead of IBetEngine.sol
Also in putBet method in IBet.sol, the @return field is missing. Consider adding one.
Always use stable pragma statement to lock the compiler version. Also there are different versions of the compiler used throughout the codebase, use only one. Finally consider upgrading the version to a newer one to use bugfixes and optimizations in the compiler.