Full Report
Oasis offers leveraged trading and borrowing, among other things. Oasis Earn had recently been added to the scope of Immunefi. Since they had audited this before with no findings, it was worth taking some time to go through the new code. Oasis Earn offers a variety of trading strategies. This is done by users keeping their funds in a private DSProxy smart wallet. To perform a trade, they delegate execution to the Earn smart contracts, which implement the trading strategy. Users send ETH to DSProxy, which will perform the specified actions for trading automatically. The Earn smart contracts use a delegateCall pattern. This allows for the editing of state variables from any location but is a very dangerous case. Earn has a large collection of Actions, such as Swap and SendToken. The real meat of this call is the executeOp and its allowances: ServiceRegistry: A mapping between service names and their ETH address. OperationRegistry: An array of allowed actions. OperationStorage: State for the operation execution. This is done because the local state belongs to the user's smart wallet. OperationExecutor: Contract called directly from the original DSProxy call. This receives an array of calls with calldata to execute the operations explained above. What's the bug then? Oasis Earn made a nice but terrible assumption: executeOp will run within the context of a user's DSProxy. What can we do with this? Storage is unique per execution (as explained above) but a selfdestruct() would be a permanent brick if we could make that happen. This attack had a few constraints that made exploitation extremely difficult and limits the options for exploitation: OperationsRegistry must be in the array of actions for each operation name. delegateCall target must be within the allowlisted service addresses. OperationExecutor storage must be empty, since we didn't call it from the proper location - DSProxy. Once here, the author thought about code reuse (ROP, JOP, etc.) attack from the binary exploitation world. The primitive is an arbitrary delegateCall to a limited set of contracts with arbitrary calldata. The list of contracts that can be called is for the entire Oasis platform and NOT just Earn. The author wrote a script to parse all of the potential functions they could jump to using this method. InitializableUpgradeabilityProxy is a proxy for the AAVE LendingPool implementation. If the data provided for the initialize call is empty, and the previous implementation is 0, then we have created a strange state confusion problem. The best part: there's a delegateCall at the end of this with data we control! Now, we've taken a limited address delegateCall and turned this into an arbitrary address! One last trick... the call we want to make will get rejected because a function verifyAction will reject it since LendingPool was not a registered action by the Oasis Earn. The function hasActionsToVerify validates that there is a list of actions; if so, it will verify. However, passing in a CustomOperation bypasses this verification while making the action still usable. Neat! The POC: Create the selfdestruct contract. Generate the calldata passed to executeOp. Call executeOp() with initialize() calldata, targetHash=InitializableUpgradeableProxy service hash,operationName = CustomOperation Hit the boom! Contract destroyed and funds are lost. Overall, a very complicated article and project! First, a design decision had unintended consequences. Then, the complexity of the modular system created the opportunity for exploitation via a classic binary exploitation technique. Pretty rad bug and exploit!
Analysis Summary
# Vulnerability: Oasis Earn Smart Contract Arbitrary DelegateCall and State Confusion
## CVE Details
- **CVE ID:** Not specified in the article
- **CVSS Score:** Not specified in the article (Context indicates High/Critical severity)
- **CWE:** Not specified in the article (Relates to improper control of generation of code / Unprotected `delegateCall`)
## Affected Systems
- **Products:** Oasis Earn (leveraged trading and borrowing platform)
- **Versions:** Not specified in the article (Identified during an Immunefi bug bounty scope review)
- **Configurations:** Systems utilizing the `OperationExecutor` contract paired with the `InitializableUpgradeabilityProxy` where `executeOp` can be invoked outside the context of a user's `DSProxy` wallet.
## Vulnerability Description
The vulnerability stems from a flawed architectural assumption: the Oasis Earn platform assumed that the `executeOp` function would only ever run within the execution context of a user's `DSProxy` smart wallet. However, `executeOp` could be called directly on the `OperationExecutor` contract. When called directly, the execution storage is completely empty.
Because the Earn smart contracts rely heavily on a modular `delegateCall` pattern, an attacker can initiate a code-reuse attack (resembling Return/Jump-Oriented Programming). The attack achieves arbitrary code execution via the following chain:
1. **Limited delegateCall:** The attacker calls `executeOp` directly. Although the application restricts `delegateCall` targets to allowlisted service addresses across the Oasis platform, it includes the `InitializableUpgradeabilityProxy` (used for the AAVE LendingPool implementation).
2. **State Confusion:** By passing empty data to the proxy's `initialize()` function when the previous implementation is 0, the attacker triggers a state confusion vulnerability. This proxy opens up a secondary `delegateCall` utilizing arbitrary, attacker-controlled calldata and targets.
3. **Verification Bypass:** The platform's action validation mechanism (`verifyAction`) normally blocks unregistered actions like the LendingPool. However, the validation check relies on `hasActionsToVerify`. By supplying a `CustomOperation` parameter, the verification logic is completely bypassed.
## Exploitation
- **Status:** PoC available (outlined in the article)
- **Complexity:** High
- **Attack Vector:** Network (Smart Contract Interaction)
### Proof of Concept (PoC) Steps:
1. Deploy a malicious contract containing a `selfdestruct()` payload.
2. Generate malicious calldata intended for `executeOp()`.
3. Invoke `executeOp()` directly with the `initialize()` calldata, setting the `targetHash` to the `InitializableUpgradeabilityProxy` service hash and the `operationName` to `CustomOperation`.
4. The proxy executes the attacker's arbitrary `delegateCall`, triggering `selfdestruct()`, permanently bricking the contract, and destroying associated funds.
## Impact
- **Confidentiality:** None
- **Integrity:** High (Permanent alteration/destruction of contract code)
- **Availability:** High (Permanent denial of service via contract destruction and total loss of funds)
## Remediation
### Patches
- Specific patch versions or commit hashes are **not specified in the article**.
### Workarounds
- Specific workarounds are **not specified in the article**. (Standard remediation for this pattern requires ensuring `executeOp` strictly enforces execution within the authorized `DSProxy` context and validating all `CustomOperation` inputs).
## Detection
- **Indicators of Compromise:** Direct transactions to the `OperationExecutor` contract originating from addresses other than authorized `DSProxy` wallets; transactions executing `CustomOperation` names that bypass `verifyAction`.
- **Detection methods and tools:** Blockchain monitoring tools can be configured to flag direct calls to `executeOp` or unexpected execution of the `selfdestruct` opcode within the platform's proxy architecture.
## References
- **Vendor advisories:** Not specified in the article
- **Relevant links:** Immunefi Bug Bounty Program (hxxps://immunefi[.]com)