Yesterday I was taking a look at oasisDEX, a trading dApp from the MakerDAO team. With a 10-100K crit bounty on Immunefi and a team with solid rep, it was worth checking out.
The dApp follows a common pattern, where user's funds are held in their own DSProxy contract, which in turn interacts with various DeFi apps (DeFiSaver works very similarly). Oasis implements a leveraging service using MultiplyProxyActions contract. Most of the details are irrelevant for understanding the vulnerability, so I will go over it very briefly. Below is my sketch of the architecture:
User sets up automatic execution logic through the oasis app, which adds a trigger to the AutoBot and sets up oasis backend triggers. When some stop-loss / take-profit / other condition occurs, oasis privileged caller calls AutomationExecutor, which results in execution chain up to the MPA contract which performs interactions with the DAI vault on behalf of the user.
This is BuyCommand's execute code:
function execute(
bytes calldata executionData,
uint256 cdpId,
bytes memory triggerData
) external {
BasicBuyTriggerData memory trigger = decode(triggerData);
validateTriggerType(trigger.triggerType, 3);
validateSelector(MPALike.increaseMultiple.selector, executionData);
executeMPAMethod(executionData);
if (trigger.continuous) {
recreateTrigger(cdpId, trigger.triggerType, triggerData);
}
}
Commands inherit from BaseMPACommand.
The issue is with the following code:
function recreateTrigger(
uint256 cdpId,
uint16 triggerType,
bytes memory triggerData
) internal virtual {
(bool status, ) = msg.sender.delegatecall(
abi.encodeWithSelector(
AutomationBot(msg.sender).addTrigger.selector,
cdpId,
triggerType,
0,
triggerData
)
);
require(status, "base-mpa-command/trigger-recreation-failed");
}
We can see that if the trigger passed to BuyCommand has the "continuous" flag, it calls the AutomationBot's addTrigger to recreate it. The problem is, BaseMPACommand assumes the sender is AutomationBot. Since execute is external(), attacker can call it by themselves, passing the continuous flag. Eventually they will get code execution in the context of the BuyCommand / SellCommand. The commands have access to user's cdp (maker vault) funds for the duration of the execution, but the access is removed after the command.execute() call from AutomationBot. So, attacker will only have access to funds from users who use the command not through the AutomationBot interface, or from an older / test version which did not release the permissions after execution.
// AutomationBot's execute()
manager.cdpAllow(cdpId, commandAddress, 1);
command.execute(executionData, cdpId, triggerData);
activeTriggers[triggerId] = TriggerRecord(0, 0);
manager.cdpAllow(cdpId, commandAddress, 0);
The vulnerability can still cause at the bare minimum a system freeze, as attacker can selfdestruct() in the BuyCommand/SellCommand context making the Bot unusable.
Being excited from this finding, I double checked this is the most up to date asset. Immunefi's impact table shows these assets:
I confirmed the Sell Command is vulnerable:
This is the same version as in oasisDEX repo:
So, everything looks legit and I went ahead and started crafting a POC. The idea was to find a legitimate command usage, copy it and change the sender to be a contract which selfdestructs(). When I traced the target TX to copy, I saw something weird - the SellCommand used is sitting on a different address!
This is the moment I realized I was being rugged. Quickly opening up this address confirmed my suspicion:
Indeed, the bug was already found one month ago and the commands were patched:
This was painful as crits are very rare to find and this one was anyone's for the taking for two months until being found. The sad part was that all the assets in scope pointed to vulnerable versions of the contract, making me waste a lot of time crafting the POC and fully analyzing everything.
The lesson learnt from the experience is to confirm the asset potentially vulnerable is the one used by actual recent TXs, as quickly as possible.
Comentarios