
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...
Share Dialog
Share Dialog
Writing about security, MEV, privacy and decentralized finance

Subscribe to 0xbanky

Subscribe to 0xbanky
<100 subscribers
<100 subscribers
After solving the first gatekeeper problem, there is a second one waiting. The problem is here
https://ethernaut.openzeppelin.com/level/0xdCeA38B2ce1768E1F409B6C65344E81F16bEc38d
The contract for this problem looks quite similar to the first one. There are multiple gates which must be passed in order to call the enter function.
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
To pass gateOne, we can use a similar approach to the previous challenge. To ensure that msg.sender != tx.origin, the contract functions can be called through a relayer contract.
Passing gateTwo is a bit more tricky. Here we see our first example of assembly code being used in a contract. Specifically here we have the use of inline assembly. This allows contract developers to write logic that is not supported by the solidity compiler. It is thus extremely useful for library creators to have access to these low level features of the EVM. Specifically, the function being used here is the extcodesize function.
After doing some research, I found that extcodesize is used to determine the size of a contract at a given address. Some people use this to ensure that functions are being called by non-contracts. One thing to note is that this call returns 0 if the contract target has not been initialized, ie. the constructor has not completed running. Therefore, it is possible to bypass this by making calls through the constructor of my relayer contract.
Finally, gateThree has some binary operation logic which must pass in order for it to work. The full operation is as follows
uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1
First, we can take a look at the right-hand side of the operation. uint64(0) - 1 causes the unsigned integer to underflow. Thus, this value is the largest possible uint64 which is 0xffffffffffffffff. Now, we can try to match up the left side of the operation with this.
The ^ operation is an XOR, which sets each bit to 1 if both inputs are 0. eg. binary 101 ^ 011 = 110. Thus, to get the output that we want, _gateKey just needs to be the exact flipped bits of bytes8(keccak256(abi.encodePacked(msg.sender)). We can get this by doing an XOR on bytes8(keccak256(abi.encodePacked(msg.sender))with 0xffffffffffffffff. My full relayer contract code looks like this
constructor (address _gatekeeperAddress) {
bytes8 negatedGateKey = bytes8(keccak256(abi.encodePacked(address(this))));
bytes8 gateKey = negatedGateKey ^ 0xffffffffffffffff;
(bool success, ) = _gatekeeperAddress.call(abi.encodeWithSignature("enter(bytes8)", gateKey));
require(success);
}
}
Thus successfully cracks the problem
After solving the first gatekeeper problem, there is a second one waiting. The problem is here
https://ethernaut.openzeppelin.com/level/0xdCeA38B2ce1768E1F409B6C65344E81F16bEc38d
The contract for this problem looks quite similar to the first one. There are multiple gates which must be passed in order to call the enter function.
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller()) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
To pass gateOne, we can use a similar approach to the previous challenge. To ensure that msg.sender != tx.origin, the contract functions can be called through a relayer contract.
Passing gateTwo is a bit more tricky. Here we see our first example of assembly code being used in a contract. Specifically here we have the use of inline assembly. This allows contract developers to write logic that is not supported by the solidity compiler. It is thus extremely useful for library creators to have access to these low level features of the EVM. Specifically, the function being used here is the extcodesize function.
After doing some research, I found that extcodesize is used to determine the size of a contract at a given address. Some people use this to ensure that functions are being called by non-contracts. One thing to note is that this call returns 0 if the contract target has not been initialized, ie. the constructor has not completed running. Therefore, it is possible to bypass this by making calls through the constructor of my relayer contract.
Finally, gateThree has some binary operation logic which must pass in order for it to work. The full operation is as follows
uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1
First, we can take a look at the right-hand side of the operation. uint64(0) - 1 causes the unsigned integer to underflow. Thus, this value is the largest possible uint64 which is 0xffffffffffffffff. Now, we can try to match up the left side of the operation with this.
The ^ operation is an XOR, which sets each bit to 1 if both inputs are 0. eg. binary 101 ^ 011 = 110. Thus, to get the output that we want, _gateKey just needs to be the exact flipped bits of bytes8(keccak256(abi.encodePacked(msg.sender)). We can get this by doing an XOR on bytes8(keccak256(abi.encodePacked(msg.sender))with 0xffffffffffffffff. My full relayer contract code looks like this
constructor (address _gatekeeperAddress) {
bytes8 negatedGateKey = bytes8(keccak256(abi.encodePacked(address(this))));
bytes8 gateKey = negatedGateKey ^ 0xffffffffffffffff;
(bool success, ) = _gatekeeperAddress.call(abi.encodeWithSignature("enter(bytes8)", gateKey));
require(success);
}
}
Thus successfully cracks the problem
No activity yet