-
Notifications
You must be signed in to change notification settings - Fork 0
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
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 573e4fd
Fix: Import statement in test is breaking the CI
oddaf 0d0ee47
chore: add custom author/reviewer headers
oddaf 8831672
fix: Added dummy function to RatesMapping.sol so it's excluded from …
oddaf d5d2810
chore: update Readme.md
oddaf 60d0c74
chore: update compiler version
oddaf 17ed085
chore: improvements to README.md
oddaf f168583
chore: Update README.md
oddaf 9ce53d5
feat: generate rates mapping from ipfs script
oddaf 0a8ff2e
chore: update readme.md
oddaf 8cf67e2
chore: revert reasons on test
oddaf 9f9149f
Update script/generate_compact_rates.py
oddaf fdc9251
chore: add license
oddaf 2b2ec4a
feat: add backwards calculation (ray per second rate -> yearly bps)
oddaf 26bd6de
forge fmt
oddaf 0f4f34e
refactor: Rename nrut -> back
oddaf e077442
chore: improve natspec for rpow
oddaf 4433f17
chore: add natspec to MAX constant and update to BPS notation
oddaf 658ba04
refactor: rename functions
oddaf 7e0aa76
Update src/Conv.sol
oddaf ebb7ce5
Update test/RatesMapping.sol
oddaf 3c0590b
Update script/generate_rates_mapping.py
oddaf 1037776
Update test/Conv.t.sol
oddaf 3382fc6
Update src/Conv.sol
oddaf bf07fc3
chore: fix compiler version in foundry.toml
oddaf ec66b36
chore: reorg folder structure
oddaf ec3f709
feat: Deployment script
oddaf 0a5248e
forge install: dss-test
oddaf 8cd925c
fix: deployment script
oddaf d7123e4
Update script/generate_compact_rates.py
oddaf 68487be
move magic number to constant BPS
oddaf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,3 +12,6 @@ docs/ | |
|
||
# Dotenv file | ||
.env | ||
|
||
# Python env | ||
venv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
```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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
amusingaxl marked this conversation as resolved.
Show resolved
Hide resolved
|
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
oddaf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)