Platform: Code4rena
Start Date: 13/11/2023
Pot Size: $24,500 USDC
Total HM: 3
Participants: 120
Period: 4 days
Judge: 0xTheC0der
Id: 306
League: ETH
Rank: 45/120
Findings: 1
Award: $19.04
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0xbrett8571, 0xepley, Bauchibred, K42, Kose, MrPotatoMagic, Myd, Sathish9098, aariiif, cats, clara, emerald7017, fouzantanveer, hunter_w3b, invitedtea, unique
19.0443 USDC - $19.04
Canto Application Specific Dollars and Bonding Curves for 1155s is an interesting system that allows creating ERC20 stablecoins pegged to NOTE and using them to back ERC1155 NFT shares.
The overall architecture centers around:
The main contracts are reasonably well modularized and make use of libraries like OpenZeppelin for security. The architecture overall seems sound.
However, through manual review several issues were identified related to access controls, input validation, withdrawal limits, and other validation.
Below is an analysis of the specific issues i discovered, their impact, root causes, risk levels, and mitigations. Critical issues that could break core invariants are highlighted first.
burn()
function does not validate the return code from redeemUnderlying()
. This means if redeeming fails, asD tokens could be burned without redeeming the underlying NOTE, breaking 1:1 peg.uint rc = cToken.redeemUnderlying(amount); require(rc == 0, "Redeem failed"); _burn(msg.sender, amount);
mint()
does not check return code from cToken.mint()
. If minting fails, contract could have imbalance between cTokens and minted asD tokens.uint rc = cToken.mint(amount); require(rc == 0, "Mint failed"); _mint(msg.sender, amount);
buy()
and sell()
functions pass _id
to to getBuyPrice()
/getSellPrice()
without validating it is a valid share ID. This can cause transactions to revert if invalid ID is provided._id
refers to existing share before passing to lookup functions:require(_id > 0 && _id <= totalShares, "Invalid id"); getBuyPrice(_id, _amount);
withdrawCarry()
does not validate _amount
is within accrued interest earnings. This allows owner to drain interest early.uint accruedInterest = calculateAccruedInterest(); require(_amount <= accruedInterest, "Exceeds accrued interest");
Contract: Market.sol
Issue: claimCreatorFee()
allows share creator to withdraw entire fee pool, not just accrued fees. Could drain funds.
Impact: Creator can withdraw unearned fees, depriving other creators. Disrupts fee model.
Likelihood: Depends on creator incentives. No controls in place.
Mitigation: Limit amount to accrued fees:
uint creatorsAccruedFees = calculateAccruedFees(msg.sender); require(amount <= creatorsAccruedFees, "Exceeds accrued fees");
Contract: Market.sol
Issue: mintNFT()
does not verify caller's token balance before minting NFTs.
Impact: Attacker mints NFTs without owning backing tokens, breaking core model.
Likelihood: High if anyone can call mintNFT().
Mitigation: Check balance before minting:
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
Contract: Market.sol
Issue: claim{Platform/Creator/Holder}Fees()
update global fee pools, which can be front-run.
Impact: Miners/bots can frontrun to drain fees before intended recipients.
Likelihood: Highly likely and profitable for miners to frontrun.
Mitigation: Use a pull-payment pattern instead of pushing funds:
function withdrawAccruedPlatformFees() external { uint amount = accruedPlatformFees[msg.sender]; accruedPlatformFees[msg.sender] = 0; msg.sender.transfer(amount); }
Contract: Market.sol
Issue: Functions like buy()
, sell()
, mintNFT()
do not validate _amount
parameter.
Impact: Potential overflow/underflow if large or negative _amount
is passed. Can corrupt state.
Likelihood: Depends on expected valid amounts. Easier if huge amounts allowed.
Mitigation: Validate _amount
:
require(_amount > 0 && _amount <= MAX_AMOUNT, "Invalid amount");
Contract: Market.sol
Issue: Division operations can cause imprecision due to uint256 data type.
Impact: Over time, accumulated precision errors could lead to meaningful loss of distributed fees.
Likelihood: Almost certain with enough transactions.
Mitigation: Use OpenZeppelin Math library for precision:
using Math for uint; uint feePortion = _fee.mul(HOLDER_CUT).div(10_000);
Contract: Market.sol
Issue: Owner has significant privileges including: whitelisting bonding curves, claiming platform fees, restricting share creation.
Impact: Owner could potentially add malicious bonding curve, change core parameters against user expectations, etc.
Likelihood: Depends on incentives of admin team, but privileged roles have potential for abuse.
Mitigation: Establish a DAO or multi-sig owner. Create dedicated whitelister role. Rotate admin credentials.
Severity: Medium
Contract: Market.sol
Issue: buy()
and sell()
perform external calls before state changes. This opens up potential reentrancy flaws.
Impact: Reentrancy would be mitigated by checks on share IDs and amounts. But optimization opportunity.
Likelihood: Low due to mitigating checks.
Mitigation: Follow check-effects-interactions pattern:
buy() { // Checks // Interactions // State changes }
I find the natspec comments useful for providing context on parameters and return values. Consider expanding to cover other functions like withdrawCarry().
Can improve clarity on platform fee percentages. Is the holder fee a subset of the creator fee?
Provide more details on expected state changes and effects in comments.
Existing unit test coverage seems limited. Consider expanding tests for:
Property-based testing could be beneficial for testing ranges of inputs.
Fuzz testing could help identify edge cases not covered by happy path tests.
The key centralized role is the owner of each asD token contract, who can:
This places trust in the owner to not withdraw excess yield or change parameters maliciously.
Recommended Mitigations:
Interest withdrawals could require timelocks, DAO approval, or be limited to accrued amounts only.
Core token parameters like name/symbol could be made immutable.
Ownership could be transferred to a DAO rather than a single address.
The Market contract owner has significant privileges:
This centralizes power and trust in the owner.
Recommended Mitigations:
Use a DAO or multi-sig wallet for collective ownership.
Create a dedicated whitelisting role with timelock rather than direct owner control.
Make fee percentages immutable after initial setup.
Adopt pull-based fee payments to limit central pooling of funds.
All shares for a specific contract depend on the same bonding curve.
This centralizes control of the price model in the bonding curve owner.
Recommended Mitigations:
Allow shares to each select their own approved bonding curve.
Make bonding curves immutable or community-controlled after launch.
Support permissionless bonding curve proposals/listings.
Here are some suggested visual diagrams to provide additional clarity on the architecture and flows.
graph TD A[asD Token] -->|1:1 pegged| B[NOTE] C[asD Factory] --> A D[Market] --> A B --> E[cNOTE] classDef grey fill:#ddd,stroke:#fff,stroke-width:2px,color:#000; class A,B,C,D grey
This shows the key components and 1:1 peg relationship between asD and NOTE.
Mint/Burn Flow
sequenceDiagram participant User participant asD participant cNOTE User->>asD: mint(amount) asD->>cNOTE: mint(amount) cNOTE-->>asD: <return code> asD-->>User: <minted asD> User->>asD: burn(amount) asD->>cNOTE: redeem(amount) cNOTE-->>asD: <return NOTE> asD-->>User: <burned asD>
Illustrates minting/burning flow and interaction with cNOTE.
Buy/Sell Flow
sequenceDiagram participant User participant Market participant BondingCurve User->>Market: buy(id,amount) Market->>BondingCurve: getBuyPrice(id,amount) BondingCurve-->>Market: <price> Market->>User: <tokens> User->>Market: sell(id,amount) Market->>BondingCurve: getSellPrice(id,amount) BondingCurve-->>Market: <price> Market->>User: <proceeds>
Shows buy/sell flow and pricing interaction with bonding curve.
The key areas to mitigate centralization risk are:
Decentralize privileged roles like whitelisting and fee control
Avoid central pooling of community funds
Empower communities to govern their own parameters
Make core share parameters immutable
Adopt pull-based fee payments
This will distribute power across stakeholders rather than concentrating it with the development team or contract owners.
8 hours
#0 - c4-judge
2023-11-29T20:44:09Z
MarioPoneder marked the issue as grade-b