Closed c4-bot-10 closed 11 months ago
raymondfam marked the issue as insufficient quality report
raymondfam marked the issue as duplicate of #54
0xA5DF marked the issue as unsatisfactory: Out of scope
Hey @0xA5DF, Thanks for judging. I don't think this is out of scope. The vulnerability lies in Ocean.sol
, which is in scope. Ocean.sol interacts with all primitives and this report points to an edge case where a primitive that registered its LpTokens in Ocean can exploit Ocean.sol _erc20Wrap
due to missing checks. And the fix is also in _erc20Wrap
on Ocean.sol.
Based on doc, registered tokens @dev These are tokens that cannot be wrapped or unwrapped
. However, _erc20Wrap
on Ocean.sol doesn't check the token being wrapped is not a registered token. A primitive that mint its own registered token and wrap it through _erc20Wrap
can exploit the accounting.
The attack path supposes a registered primitive to mint its own registered Lptoken and wrap it in Ocean.sol, every time a user deposit liquidity (swapping out registered Lptoken). A registered primitive minting its own registered Lptoken in Ocean.sol shouldn't be allowed, because it enables the primitive to accumulate 'ghost balance' of Lptoken in Ocean system. The 'ghost balance' can be repurposed to profit from other primitives in Ocean.
This is an exploit of _erc20Wrap
's vulnerability.
See the comment in the primary issue, this is OOS because this is a known issue that was listed in the contest description (README.md
)
Lines of code
https://github.com/code-423n4/2023-11-shellprotocol/blob/485de7383cdf88284ee6bcf2926fb7c19e9fb257/src/ocean/Ocean.sol#L836 https://github.com/code-423n4/2023-11-shellprotocol/blob/485de7383cdf88284ee6bcf2926fb7c19e9fb257/src/ocean/Ocean.sol#L1039-L1044
Vulnerability details
Impact
A malicious native primitive can exploit the Ocean.sol's accounting of registered tokens through users'
computeOutputAmount
transactions, and exploit other natively registered liquidity pools for profits.Proof of Concept
In Ocean.sol's accounting process, Ocean's native primitives and ocean adapters are handled differently.
For ocean adapters that wrap external liquidity pools,
unwrapToken()
(OceanAdapter.sol) andwrapToken()
(OceanAdapter.sol) are needed during_computeOutputAmount
or_computeInputAmount
interactions. And Ocean.sol will also force to mint wrapped inputTokens and burn wrapped outputTokens from the adapters, which is done through_increaseBalanceOfPrimitive
and_decreaseBalanceOfPrimitive
. For Ocean's natively registered primitives, Ocean.sol will not mint or burn if the inputToken or outputToken is registered by the same primitive concerned. This is achieved throughif (_isNotTokenOfPrimitive(outputToken, primitive) && (outputAmount > 0))
bypass.The double standard makes sense because based on code Doc in OceanERC1155.sol
registerNewTokens()
:@dev These are tokens that cannot be wrapped or unwrapped
. The assumption is that when a primitive registers the outputToken, it doesn't need to wrap or unwrap beforehand or duringcomputeOutputToken()
/computeInputToken()
. The balance change is directly tracked in Ocean.sol. At the end ofcompute*()
, Ocean will simply mint the outputToken for the user without burning the outputToken from the registered primitive.There are (2) vulnerabilities here: (1) Ocean.sol only assumes that a native primitive that registered the outputToken doesn't need to wrap the registered token, but doesn't check that the native primitive will NOT wrap the registered outputToken; (2) Ocean.sol will in fact allow a native primitive to wrap the registered outputToken at any time through
_erc20Wrap()
;(https://github.com/code-423n4/2023-11-shellprotocol/blob/485de7383cdf88284ee6bcf2926fb7c19e9fb257/src/ocean/Ocean.sol#L820)
(https://github.com/code-423n4/2023-11-shellprotocol/blob/485de7383cdf88284ee6bcf2926fb7c19e9fb257/src/ocean/Ocean.sol#L1043-L1044)
These two open up attack vectors for a malicious Ocean native primitive to wrap its own registered outputTokens during
compute*()
calls.Suppose this example:
The malicious primitive contract needs to a) inherit ERC20.sol, b) and inherit
src/proteus/LiquidityPool.sol
which will register its LpToken in Ocean. c) able to receive ERC1155 tokens, and transfer ERC1155 it received to an EOA.Step 1: Malicious PrimitiveA register in Ocean by calling
registerNewTokens()
, which register is address as LpToken. Suppose the registered LpToken id is OceanIdA. PrimitiveA offers token swaps and liquidity management. xToken is WBTC, yToken is USDC.Step 2: Before the attack, PrimitiveA will need to implement wrapping its LPToken OceanIdA in
computeInputAmount()
andcomputOutputAmount()
. For example:Inside
wrapToken()
, PrimitiveA will mint its LPToken for outputAmount and approve Ocean.sol for outputAmount, before calling Ocean.sol to_erc20Wrap()
. For example:Now in
compute*()
, PrimitiveA will mint and transfer LpToken to Ocean.sol, in return Ocean.sol mint the wrapped OceanIdA for PrimitiveA's address balance.For now on any user who deposits WBTC or USDC into primitiveA through Ocean.sol, the following will happen: a) the user will wrap WBTC and USDC into Ocean; b) Ocean will mint wrapped WBTC / USDC in the user address; c) In the same
compute*()
, PrimitiveA will mint and transfer LpToken to Ocean.sol; d) Ocean.sol increase PrimitveA's OceanIdA balance by amount; e) Ocean.sol will also increase the user's OceanIdA balance by amount;(Note: Although LpToken in Step 2 is transferred to Ocean.sol, Ocean doesn't own these tokens and has no means to sweep tokens or regulate PrimitiveA's balance.)
Step 3: Overtime more users add liquidity to PrimtiveA. And PrimitiveA will accumulate its balance of wrapped OceanIdA in Ocean.sol. PrimitiveA's balance mirrored users combined OceanIdA balance.
Step 4: Case 1:Other ocean native primitives adopt OceanIdA as part of their accepted swap tokens. Suppose PrimitiveB supports Eth, Frax and OceanIdA as swap tokens and its LPToken id is OceanIdB. (See a similar scenario in the integration test: test/integration/Interaction.js, where poolB is deployed with poolA's lpToken as a swap token )
There will be users who add liquidity to PrimitiveB, depositing Eth, Frax or OceanIdA.
Now malicious primitive can mobilize its accumulated OceanIdA, transferring the wrapped OceanIdA to an EOA. The EOA will use its wrapped OceanIdA balance to swap out Eth or Frax. The EOA will unwrap the swapped-out Eth or Frax for profits.
Step 4: Case 2: The malicious PrimitiveA can also create and register another ocean-native liquidity pool that supports Eth, Frax and OceanIdA. The rest is the same as Case 1.
The attacker can potentially open multiple Ocean native liquidity pools that support a diversity of popular ERC20 tokens and perform the same swap for diversified profits.
It should be noted that it’s the ocean’s accounting that is compromised to enable the attack.
This is because registered LpTokens are not intended to be wrapped or unwrapped as stated in the code doc(
registerNewTokens()
). They only exist for balance accounting and are essentially ghost tokens. However, attacker can still mint an ERC20 lpToken and force wrap it with Ocean.sol, this contradicts the core accounting premise of Ocean. In this case, the total balance increase of OceanIdA doubles each time there is an add-liquidity by a user into the malicious primitive.I consider this high due to the erosion of Ocean.sol's accounting premise.
Tools Used
Manual Review
Recommended Mitigation Steps
In Ocean.sol, check and revert when a native primitive tries to wrap its own LpToken. For example:
Assessed type
Other