Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Use compressed storage #1

Merged
merged 31 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e1a8cfa
refactor: Use compressed storage
oddaf Jan 22, 2025
573e4fd
Fix: Import statement in test is breaking the CI
oddaf Jan 22, 2025
0d0ee47
chore: add custom author/reviewer headers
oddaf Jan 22, 2025
8831672
fix: Added dummy function to RatesMapping.sol so it's excluded from …
oddaf Jan 22, 2025
d5d2810
chore: update Readme.md
oddaf Jan 24, 2025
60d0c74
chore: update compiler version
oddaf Jan 24, 2025
17ed085
chore: improvements to README.md
oddaf Jan 24, 2025
f168583
chore: Update README.md
oddaf Feb 1, 2025
9ce53d5
feat: generate rates mapping from ipfs script
oddaf Feb 1, 2025
0a8ff2e
chore: update readme.md
oddaf Feb 1, 2025
8cf67e2
chore: revert reasons on test
oddaf Feb 1, 2025
9f9149f
Update script/generate_compact_rates.py
oddaf Feb 3, 2025
fdc9251
chore: add license
oddaf Feb 5, 2025
2b2ec4a
feat: add backwards calculation (ray per second rate -> yearly bps)
oddaf Feb 5, 2025
26bd6de
forge fmt
oddaf Feb 5, 2025
0f4f34e
refactor: Rename nrut -> back
oddaf Feb 5, 2025
e077442
chore: improve natspec for rpow
oddaf Feb 5, 2025
4433f17
chore: add natspec to MAX constant and update to BPS notation
oddaf Feb 5, 2025
658ba04
refactor: rename functions
oddaf Feb 7, 2025
7e0aa76
Update src/Conv.sol
oddaf Feb 7, 2025
ebb7ce5
Update test/RatesMapping.sol
oddaf Feb 12, 2025
3c0590b
Update script/generate_rates_mapping.py
oddaf Feb 12, 2025
1037776
Update test/Conv.t.sol
oddaf Feb 12, 2025
3382fc6
Update src/Conv.sol
oddaf Feb 12, 2025
bf07fc3
chore: fix compiler version in foundry.toml
oddaf Feb 12, 2025
ec66b36
chore: reorg folder structure
oddaf Feb 12, 2025
ec3f709
feat: Deployment script
oddaf Feb 12, 2025
0a5248e
forge install: dss-test
oddaf Feb 12, 2025
8cd925c
fix: deployment script
oddaf Feb 12, 2025
d7123e4
Update script/generate_compact_rates.py
oddaf Feb 12, 2025
68487be
move magic number to constant BPS
oddaf Feb 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ docs/

# Dotenv file
.env

# Python env
venv
66 changes: 29 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,58 @@
## Foundry
## Conv

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
**Onchain repository for DSS rates**

Foundry consists of:
Conv stores all per-second DSS rates for annualized BPSs in a single on-chain repository.

- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
### Motivation

## Documentation
Useful for validation using human-friendly notation, which drastically reduces the cognitive overhead when checking rates.

https://book.getfoundry.sh/
Requirements:
- The rates need to have full precision compared to rates currently used in DSS (https://ipfs.io/ipfs/QmVp4mhhbwWGTfbh2BzwQB9eiBrQBKiqcPRZCaAxNUaar6)
- Read cost should be reasonable, allowing other components of the system to use it without too much overhead.
- The contract needs to be deployable efficiently (low priority, one time cost).

## Usage
### Design

### Build
We explored several ways to store or calculate rates onchain and arrive at this approach, for details see [this](https://github.com/dewiz-xyz/conv-research).

```shell
$ forge build
```
The contract makes use of optimized storage to ease the cost of deployment. Each rate is stored as `rate - RAY`, so only the relevant part of the rate takes space in storage. Each rate is stored in 8 bytes, so every storage position fits exactly four rates.

### Test
On reads, the function `turn(bps)` will fetch the correct storage slot, fetch the desired rate within it, add one RAY and return the result

```shell
$ forge test
```
### Limitations

### Format
- Since rates are stored in 8 bytes, the max BPS that can be used without reimplementing this contract is **7891**.
- EIP-170: Due to contract size limits on Ethereum mainnet, the ceiling for rates is 6k. On L2s that do not enforce the limit this does not apply.
- Gas cost of deployment: With the current 30M block gas ceiling on Ethereum mainnet, up to ~5.5k rates can be stored.
Copy link
Member

@0x3phemeralsoul 0x3phemeralsoul Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it make sense to translate 5.5K rates into bps? so from 0.01% until approx which bps would be possible, is it 50% approx? This would give a better idea of the technical limitation from a business sense

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end the limit is set 50% so we don't use the full 30M gas ceiling.
It means that it's every bps from 0 to 50%, a total of 50001 rates.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first ceiling is the block gas limit. It only allows rates until some number around 55%. I went with 50 because it's round (easy to remember) and I think going from it to 54.12 wouldn't make much difference in practice. But we could go there if needed. If the gas limit is raised we could go to the next ceiling (contract size, could go up to 6k rates)


```shell
$ forge fmt
```
## Deployments

### Gas Snapshots
- **5000bps Ethereum Mainnet**: tbd

```shell
$ forge snapshot
```
## Usage

### Anvil
### Build

```shell
$ anvil
$ forge build
```

### Deploy
### Test

```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
$ forge test
```

### Cast
### Gas Snapshots

```shell
$ cast <subcommand>
$ forge snapshot
```

### Help
### Deploy

```shell
$ forge --help
$ anvil --help
$ cast --help
```
$ forge create Conv --broadcast
```
6 changes: 6 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ src = "src"
out = "out"
libs = ["lib"]

optimizer = true
optimizer_runs = 1

[fuzz]
runs = 1000000

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
59 changes: 0 additions & 59 deletions script/Deploy.s.sol

This file was deleted.

111 changes: 111 additions & 0 deletions script/generate_compact_rates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3

import struct

def int_to_bytes8(n: int) -> str:
"""Convert integer to 8-byte hex string in big-endian format for Solidity."""
# Convert to hex, remove '0x' prefix, pad to 16 chars (8 bytes)
return format(n & ((1 << 64) - 1), '016x')

def parse_rates_mapping(file_path, max_bps=5000):
"""Parse rates from RatesMapping.sol up to max_bps."""
rates = {}
with open(file_path, 'r') as f:
for line in f:
if 'rates[' in line and '] =' in line:
parts = line.strip().split('rates[')[1].split('] =')
bps = int(parts[0])
if bps <= max_bps: # Only include rates up to max_bps
rate = int(parts[1].strip().rstrip(';'))
rates[bps] = rate
return rates

def pack_rates(rates):
packed = bytearray()
for rate in rates:
# Pack each rate as a full 8-byte value
packed.extend(rate.to_bytes(8, 'big'))
return bytes(packed)

def generate_contract() -> str:
"""Generate compact bytes representation and contract for all rates in RatesMapping.sol."""
RAY = 10**27
all_bytes = []

# Get rates from RatesMapping.sol
rates = parse_rates_mapping('test/RatesMapping.sol')

# Sort rates by bps to ensure correct order
sorted_bps = sorted(rates.keys())
start_bps = sorted_bps[0]
end_bps = sorted_bps[-1]

# Generate rates based on the mapping, ensuring 4 rates per word
for i in range(0, len(sorted_bps), 4):
word_rates = []
# Get next 4 rates (or pad with zeros if at the end)
for j in range(4):
if i + j < len(sorted_bps):
bps = sorted_bps[i + j]
rate = rates[bps]
# Store rate - RAY, ensure it fits in uint64
adjusted_rate = rate - RAY
if adjusted_rate >= (1 << 64):
raise ValueError(f"Rate difference too large for bps {bps}: {adjusted_rate}")
hex_rate = int_to_bytes8(adjusted_rate)
else:
# Pad with zeros if we don't have enough rates
hex_rate = '0' * 16
word_rates.append(hex_rate)
all_bytes.extend(word_rates)

# Join all bytes into one big hex string without length prefix
compact_bytes = f'hex"{"".join(all_bytes)}"'

# Create the contract
contract_template = f'''// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

contract Conv {{
uint256 constant public MAX = {end_bps};
uint256 constant internal RAY = 10**27;

// Each rate takes 8 bytes (64 bits), total of {len(sorted_bps)} rates
// Each storage word (32 bytes) contains exactly 4 rates
// Total size = {len(sorted_bps)} * 8 = {len(sorted_bps) * 8} bytes
bytes internal RATES;

constructor() {{
RATES = {compact_bytes};
}}

/// @notice Fetches the rate for a given basis points value
/// @param bps The basis points value to get the rate for
/// @return rate The annual rate value
function turn(uint256 bps) public view returns (uint256 rate) {{
require(bps <= MAX);

assembly {{
let offset := mul(bps, 8) // Each rate is 8 bytes
let wordPos := div(offset, 32) // Which 32-byte word to read
let bytePos := mod(offset, 32) // Position within the word

let dataSlot := keccak256(RATES.slot, 0x20)

let value := sload(add(dataSlot, wordPos))

let shifted := shr(mul(sub(24, bytePos), 8), value)

rate := add(and(shifted, 0xFFFFFFFFFFFFFFFF), RAY)
}}
}}
}}'''
return contract_template

def main():
"""Generate and write the contract."""
contract = generate_contract()
print(contract)

if __name__ == '__main__':
main()
Loading