Platform: Code4rena
Start Date: 21/12/2023
Pot Size: $90,500 USDC
Total HM: 10
Participants: 39
Period: 18 days
Judge: LSDan
Total Solo HM: 5
Id: 315
League: ETH
Rank: 12/39
Findings: 2
Award: $437.10
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: hash
Also found by: 0xTheC0der, HChang26, c0pp3rscr3w3r
415.1971 USDC - $415.20
Judge has assessed an item in Issue #146 as 3 risk. The relevant finding follows:
L-6: OLAS minting via treasury is not guaranteed The OLAS.mint(…) method does not revert if the requested amount cannot be limited due to the inflation limit: function mint(address account, uint256 amount) external { // Access control if (msg.sender != minter) { revert ManagerOnly(msg.sender, minter); }
// Check the inflation schedule and mint if (inflationControl(amount)) { _mint(account, amount); } }
The treasury itself correctly accounts for the OLAS inflation limit and thereby guarantees the minting of the the requested OLAS amount here and here. However, the minter of OLAS can be changed via changeMinter(address newMinter) by the owner. Then, the new minter can mint OLAS up to the inflation limit. Next, the minter is changed back to the treasury contract. Now, the minting via treasury (see references in step 2) will fail to mint enough OLAS without any error.
#0 - c4-judge
2024-01-23T12:48:33Z
dmvt marked the issue as duplicate of #373
#1 - c4-judge
2024-01-23T12:48:36Z
dmvt marked the issue as partial-25
🌟 Selected for report: IllIllI
Also found by: 0x11singh99, 0xA5DF, 0xMilenov, 0xTheC0der, 7ashraf, Bauchibred, EV_om, Kaysoft, Sathish9098, SpicyMeatball, cheatc0d3, erebus, hash, imare, immeas, joaovwfreire, lil_eth, lsaudit, oakcobalt, para8956, peanuts, rvierdiiev, slvDev, trachev
21.8971 USDC - $21.90
setFxChildTunnel
/setFxRootTunnel
after deploymentOLAS
supply capselfdestruct
Depository.deposit(...)
by front-runnerTreasury.drainServiceSlashedFunds()
can leave dust behind in ServiceRegistry
GuardCM.checkAfterExecution(...)
setFxChildTunnel
/setFxRootTunnel
after deploymentThe FxERC20ChildTunnel.setFxRootTunnel(...) and FxERC20RootTunnel.setFxChildTunnel(...) methods need to be called immediately after deployment of these 2 contracts in order to finish their initialization.
However, those methods' access is not restricted:
function setFxRootTunnel(address _fxRootTunnel) external virtual { require(fxRootTunnel == address(0x0), "FxBaseChildTunnel: ROOT_TUNNEL_ALREADY_SET"); fxRootTunnel = _fxRootTunnel; } function setFxChildTunnel(address _fxChildTunnel) public virtual { require(fxChildTunnel == address(0x0), "FxBaseRootTunnel: CHILD_TUNNEL_ALREADY_SET"); fxChildTunnel = _fxChildTunnel; }
As a consequence, the initialization can be front-run by an attacker after deployment to redirect the tunnels.
However, this attack can only happen after deployment before the protocol is in use and needs to go unnoticed for a while to cause any damage. Otherwise, redeployment is straight-forward in case this happens.
Because those 2 tunnel contracts are dependent on each other's address. One is on Ethereum, the other on Polygon.
Deploy those contracts via create2
with a salt in order to ensure deterministic contract addresses before deployment.
Therefore, the pre-computed root/child tunnel address can already be provided via the constructor which avoids the need for a subsequent initialization call that can be front-run.
OLAS
supply capThe TokenomicsConstants.getSupplyCapForYear(...) method has pre-defined progressive supply cap levels for the first 10 years and 2% inflation afterwards, while the OLAS.inflationRemainder() method, which actually enforces the supply cap on minting, has the same fixed supply cap for the first 10 years and 2% inflation afterwards.
Therefore there is a discrepancy between the amount of OLAS
which is expected to be minted vs. the amount which actually can be minted within the first 10 years.
The expected supply cap levels for the first 10 years from TokenomicsConstants.sol
:
uint96[10] memory supplyCaps = [ 529_659_000_00e16, 569_913_084_00e16, 641_152_219_50e16, 708_500_141_72e16, 771_039_876_00e16, 828_233_282_97e16, 879_860_040_11e16, 925_948_139_65e16, 966_706_331_40e16, 1_000_000_000e18 ];
The actual supply cap for the first 10 years from OLAS.sol
:
uint256 public constant tenYearSupplyCap = 1_000_000_000e18;
selfdestruct
According to Treasury.sol:
/// invariant {:msg "broken conservation law"} address(this).balance == ETHFromServices + ETHOwned;
Although the contract's constructor and methods perfectly respect this invariant, one can simply force-fund the contract with ETH
using the selfdestruct EVM opcode which consequently breaks the given invariant.
Recommendation: Add a sync()
method which adds any surplus balance to the ETHOwned
state variable while respecting the type(uint96).max
limit, see also receive().
A blacklisted donator can simply circumvent the blacklisting by using another account for donations. This is already proven within the test case "Claim incentives for unit owners when donator and service owner are different accounts" which shows that the donator can be an unrelated account.
The only point where the actual address of the donator is important is in Tokenomics._trackServiceDonations(...):
bool topUpEligible; if (incentiveFlags[2] || incentiveFlags[3]) { address serviceOwner = IToken(serviceRegistry).ownerOf(serviceIds[i]); topUpEligible = (IVotingEscrow(ve).getVotes(serviceOwner) >= veOLASThreshold || IVotingEscrow(ve).getVotes(donator) >= veOLASThreshold) ? true : false; }
Here, the top-up eligibility is determined based on the votes of the service owner or donator.
Therefore, the blacklisting is only effective if the service owner does not have enough votes and the alternative donator (to circumvent blacklist) does not have enough votes either.
Depository.deposit(...)
by front-runnerAnyone can front-run a user's Depository.deposit(uint256 productId, uint256 tokenAmount) transaction.
However, this is especially a problem whem the user desires a full deposit where tokenAmount
is chosen such that the whole product.supply
is used. In this case, the attacker can front-run the transaction with a miniscule tokenAmount
in order to cause DoS for the user via a ProductSupplyLow error.
OLAS
minting via treasury is not guaranteedfunction mint(address account, uint256 amount) external { // Access control if (msg.sender != minter) { revert ManagerOnly(msg.sender, minter); } // Check the inflation schedule and mint if (inflationControl(amount)) { _mint(account, amount); } }
OLAS
amount here and here.minter
of OLAS
can be changed via changeMinter(address newMinter) by the owner
.minter
can mint OLAS
up to the inflation limit.OLAS
without any error.Treasury.drainServiceSlashedFunds()
can leave dust behind in ServiceRegistry
Since the drainServiceSlashedFunds() method enforces the minAcceptedETH
limit before draining funds from the ServiceRegistry
. There can be occasions where small amounts (< minAcceptedETH
) are left behind in the ServiceRegistry
without any possibility to withdraw them.
Recommendation: Do not enforce minAcceptedETH
when draining slashed funds.
GuardCM.checkAfterExecution(...)
The implementation of the GuardCM.checkAfterExecution(...) method, which is the counterpart to GuardCM.checkTransaction(...)
, is empty.
There might be settings within the protocol that should not be changed via a multisig
call. Thus, it is recommended to check for such invariants after the call within that method and revert if necessary.
#0 - c4-pre-sort
2024-01-10T14:44:31Z
alex-ppg marked the issue as sufficient quality report
#1 - alex-ppg
2024-01-10T14:51:01Z
L-1 dupe of #404 L-2 dupe of #79 L-4 dupe of #311 L-6 dupe of #390
#2 - c4-judge
2024-01-19T18:51:35Z
dmvt marked the issue as grade-b
#3 - MarioPoneder
2024-01-22T14:30:13Z
Dear Judge, Would L-6 be eligible for duplication to #390? It points out the core issue of mint not reverting when inflation control kicks in and references the 2 main points of potential user loss in point 2. of the description.
Kind regards,
0xTheC0der