Skip to content

Commit

Permalink
feat: rewrite the entire project in Cairo (1) (#58)
Browse files Browse the repository at this point in the history
Rewrites all the contracts and tests in Cairo, aka. Cairo 1.
  • Loading branch information
xJonathanLEI committed Jul 29, 2023
1 parent 89d2fde commit 482d14f
Show file tree
Hide file tree
Showing 98 changed files with 7,362 additions and 7,262 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/artifacts-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,27 @@ jobs:
uses: actions/checkout@v2
with:
submodules: true

- name: Generate artifacts
run: |
./scripts/compile_with_docker.sh
- name: "Set up deploy key for artifacts repo"
uses: "webfactory/ssh-agent@v0.7.0"
with:
ssh-private-key: ${{ secrets.ARTIFACTS_DEV_KEY }}

- name: Push artifacts
run: |
COMMIT_HASH="$(git log -1 --format="%H")"
git clone --depth=1 "${{ secrets.ARTIFACTS_DEV_GIT_URL }}" ./artifacts
git clone --depth=1 "git@github.com:zkLend/zklend-contract-artifacts-dev" ./artifacts
mkdir -p ./artifacts/v1-core
cp -r ./build ./artifacts/v1-core/$COMMIT_HASH
rm -rf ./artifacts/v1-core/latest
(cd ./artifacts/v1-core/ && rm -rf ./latest && ln -s ./$COMMIT_HASH ./latest)
cd ./artifacts
git config user.name "${{ secrets.ARTIFACTS_DEV_GIT_NAME }}"
git config user.email "${{ secrets.ARTIFACTS_DEV_GIT_EMAIL }}"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "artifacts: add contract artifacts from CI"
git push
13 changes: 10 additions & 3 deletions .github/workflows/artifacts-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,27 @@ jobs:
uses: actions/checkout@v2
with:
submodules: true

- name: Generate artifacts
run: |
./scripts/compile_with_docker.sh
- name: "Set up deploy key for artifacts repo"
uses: "webfactory/ssh-agent@v0.7.0"
with:
ssh-private-key: ${{ secrets.ARTIFACTS_RELEASE_KEY }}

- name: Push artifacts
run: |
COMMIT_HASH="$(git log -1 --format="%H")"
git clone --depth=1 "${{ secrets.ARTIFACTS_RELEASE_GIT_URL }}" ./artifacts
git clone --depth=1 "git@github.com:zkLend/zklend-contract-artifacts" ./artifacts
mkdir -p ./artifacts/v1-core
cp -r ./build ./artifacts/v1-core/$COMMIT_HASH
rm -rf ./artifacts/v1-core/latest
(cd ./artifacts/v1-core/ && rm -rf ./latest && ln -s ./$COMMIT_HASH ./latest)
cd ./artifacts
git config user.name "${{ secrets.ARTIFACTS_RELEASE_GIT_NAME }}"
git config user.email "${{ secrets.ARTIFACTS_RELEASE_GIT_EMAIL }}"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add .
git commit -m "artifacts: add contract artifacts from CI"
git push
25 changes: 3 additions & 22 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,12 @@ jobs:
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Use Python 3.9
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install prettier
run: |
yarn global add prettier@2.8.7
- name: Install black
- name: Check prettier format
run: |
pip install black==23.3.0
- name: Install pyright
run: |
pip install pyright
- name: Install cairo-lang
run: |
pip install cairo-lang==0.11.0.2
- name: Check Python format
run: |
black --check "./tests"
- name: Check Yaml format
run: |
prettier --check "**/*.{yaml,yml}"
prettier --check .
- name: Check Cairo format
run: |
cairo-format -c ./src/zklend/*.cairo ./src/zklend/**/*.cairo ./src/zklend/**/**/*.cairo
- name: Run Python lints
run: |
pyright "./tests"
./scripts/check_format_with_docker.sh
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
.pytest_cache/
__pycache__/
/build/
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

3 changes: 0 additions & 3 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
.pytest_cache/
__pycache__/
/build/
/dependencies/
26 changes: 3 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,6 @@

## Getting started

### Cloning

This repository uses Git submodules for managing dependencies. Use the `--recursive` flag to clone the repository with submodules:

```sh
$ git clone --recursive https://github.com/zkLend/zklend-v1-core
```

If you already cloned the repostiroy without submodules, execute this command inside the repostiroy to fetch the submodules:

```sh
$ git submodule update --init --recursive
```

### Compiling

To stay as flexible as possible, this repository is not using any smart contract development framework at the moment and invokes `starknet-compile` directly for compiling contracts. A [helper script](./scripts/compile.sh) is available for compiling all the contracts:
Expand All @@ -31,7 +17,7 @@ To stay as flexible as possible, this repository is not using any smart contract
$ ./scripts/compile.sh
```

Note that the script requires [cairo-lang](https://github.com/starkware-libs/cairo-lang) to be [installed](https://www.cairo-lang.org/docs/quickstart.html).
Note that the script requires the `starknet-compile` command from [starkware-libs/cairo](https://github.com/starkware-libs/cairo) to be installed.

Alternatively, the compilation process can be done inside a Docker container for deterministic output:

Expand All @@ -43,16 +29,10 @@ In either case, contract artifacts are generated in the `build` folder.

### Running tests

`pytest` and `pytest-asyncio` must be installed to run tests. `pytest-xdist` is also needed if you want to run tests in parallel:

```sh
$ pip install pytest pytest-asyncio pytest-xdist
```

To run tests:
The `cairo-test` command from [starkware-libs/cairo](https://github.com/starkware-libs/cairo) must be installed to run tests. To run the tests:

```sh
$ pytest -n 16 -v ./tests/*_test.py ./tests/**/*_test.py
$ cairo-test --starknet .
```

Alternatively, run the tests inside a Docker container:
Expand Down
3 changes: 3 additions & 0 deletions cairo_project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[crate_roots]
zklend = "src"
tests = "tests"
1 change: 0 additions & 1 deletion dependencies/cairo-contracts
Submodule cairo-contracts deleted from 70cbd0
10 changes: 10 additions & 0 deletions scripts/check_format_with_docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

SCRIPT_DIR=$( cd -- "$( dirname "$0" )" &> /dev/null && pwd )
REPO_ROOT=$( cd -- "$( dirname $( dirname "$0" ) )" &> /dev/null && pwd )

docker run --rm \
-v "${REPO_ROOT}:/work" \
--entrypoint "cairo-format" \
starknet/cairo:2.1.0-rc2 \
-c -r /work
28 changes: 12 additions & 16 deletions scripts/compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,27 @@ SCRIPT_DIR=$( cd -- "$( dirname "$0" )" &> /dev/null && pwd )
REPO_ROOT=$( cd -- "$( dirname $( dirname "$0" ) )" &> /dev/null && pwd )

compile () {
SOURCE="$1"
SOURCE_DIR="$(dirname "$SOURCE")"
SOURCE_FILE="$(basename -- "$SOURCE")"
OUTPUT_DIR="../build/$SOURCE_DIR"
OUTPUT="$OUTPUT_DIR/${SOURCE_FILE%.*}.json"
MODULE="$1"
NAME="$2"
OUTPUT="$REPO_ROOT/build/$NAME.json"

echo "Compiling $(realpath $SOURCE)"
echo "Compiling $MODULE::$NAME"

mkdir -p "$OUTPUT_DIR"

# Ignores debug info for smaller artifacts
starknet-compile-deprecated --no_debug_info $SOURCE > $OUTPUT
# This is better than using the output option, which does not emit EOL at the end.
starknet-compile -c "$MODULE::$NAME" $REPO_ROOT > $OUTPUT

if [ -n "$USER_ID" ] && [ -n "$GROUP_ID" ]; then
chown $USER_ID:$GROUP_ID $OUTPUT_DIR
chown $USER_ID:$GROUP_ID $OUTPUT
fi
}

cd "$REPO_ROOT/src"
mkdir -p "$REPO_ROOT/build/zklend"
mkdir -p "$REPO_ROOT/build"

find -type f -name '*.cairo' | while read SOURCE; do
compile "$SOURCE"
done
compile zklend::market Market
compile zklend::z_token ZToken
compile zklend::default_price_oracle DefaultPriceOracle
compile zklend::irms::default_interest_rate_model DefaultInterestRateModel
compile zklend::oracles::empiric_oracle_adapter EmpiricOracleAdapter

if [ -n "$USER_ID" ] && [ -n "$GROUP_ID" ]; then
chown -R $USER_ID:$GROUP_ID "$REPO_ROOT/build"
Expand Down
10 changes: 1 addition & 9 deletions scripts/compile_with_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,5 @@ docker run --rm \
--env "USER_ID=$(id -u)" \
--env "GROUP_ID=$(id -g)" \
--entrypoint sh \
starknet/cairo-lang:0.11.0.2 \
starknet/cairo:2.1.0-rc2 \
-c "cd /work && ./scripts/compile.sh"

# Using prettier instead of `jq` due to known issue:
# https://github.com/xJonathanLEI/starknet-rs/issues/76#issuecomment-1058153538
docker run --rm \
-v "$REPO_ROOT/build:/work" \
--user root \
tmknom/prettier:2.6.2 \
--write .
6 changes: 0 additions & 6 deletions scripts/entrypoints/run_tests.sh

This file was deleted.

6 changes: 3 additions & 3 deletions scripts/run_tests_with_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SCRIPT_DIR=$( cd -- "$( dirname "$0" )" &> /dev/null && pwd )
REPO_ROOT=$( cd -- "$( dirname $( dirname "$0" ) )" &> /dev/null && pwd )

docker run --rm \
-v "${SCRIPT_DIR}/entrypoints/run_tests.sh:/entry.sh:ro" \
-v "${REPO_ROOT}:/work" \
--entrypoint "/entry.sh" \
starknet/cairo-lang:0.11.0.2
--entrypoint "cairo-test" \
starknet/cairo:2.1.0-rc2 \
--starknet /work
14 changes: 7 additions & 7 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Smart Contracts

This folder hosts all the smart contract source code, including zkLend contracts and dependencies.
This folder hosts all the zkLend smart contract source code. zkLend smart contracts do not use any external dependencies.

## System overview

Expand All @@ -10,20 +10,20 @@ Here's a high-level diagram of the system architecture, including non-smart-cont
<img src="../images/system-overview.svg?raw=true" alt="System overview"/>
</p>

At the center of the system is the [Market](./zklend/Market.cairo) contract, which serves as the entrypoint for most user operations. It maintains user data, enforces invariants, and communicates with other smart contracts.
At the center of the system is the [Market](./market.cairo) contract, which serves as the entrypoint for most user operations. It maintains user data, enforces invariants, and communicates with other smart contracts.

Another contract that users would often interact with is the [ZToken](./zklend/ZToken.cairo) contract, which is essentially an interest-bearing deposit certificate for assets deposited into the system. It's ERC20-compliant but unlike most ERC20 tokens with static balances, ZToken balances grow over time as interest accures such that it can always be exchanged 1:1 against the underlying asset.
Another contract that users would often interact with is the [ZToken](./z_token.cairo) contract, which is essentially an interest-bearing deposit certificate for assets deposited into the system. It's ERC20-compliant but unlike most ERC20 tokens with static balances, ZToken balances grow over time as interest accures such that it can always be exchanged 1:1 against the underlying asset.

The remaining two contracts that get deployed are the [PriceOracle](./zklend/PriceOracle.cairo) and [DefaultInterestRateModel](./zklend/irms/DefaultInterestRateModel.cairo), which are implementation details that users do not directly interface with.
The remaining two contracts that get deployed are the [DefaultPriceOracle](./default_price_oracle.cairo) and [DefaultInterestRateModel](./irms/default_interest_rate_model.cairo), which are implementation details that users do not directly interface with.

## Upgradeability

To enable rapid iteration during the early stage of the protocol, some smart contracts have been designed to be upgradeable, specifically:

- [Market](./zklend/Market.cairo)
- [ZToken](./zklend/ZToken.cairo)
- [Market](./market.cairo)
- [ZToken](./z_token.cairo)

When these contracts are deployed, their classes are first declared, which will then be used as implementation classes inside the [Proxy](./zklend/Proxy.cairo) contract. Technically speaking, these upgradeable contracts are never really _deployed_, but only _declared_.
The upgradeability is enabled by the `replace_class` syscall. Thanks to the syscall, there's no need to use the proxy pattern at all, as the contract itself can change its own implementation.

The rest of the contracts are immutable as they tend to hold a small state, making them trivial to be redeployed altogether.

Expand Down
96 changes: 96 additions & 0 deletions src/default_price_oracle.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/// A central oracle hub for connecting to different upstream oracles and exposing a single getter
/// to the core protocol.
#[starknet::contract]
mod DefaultPriceOracle {
use starknet::ContractAddress;

// Hack to simulate the `crate` keyword
use super::super as crate;

use crate::interfaces::{
IDefaultPriceOracle, IPriceOracle, IPriceOracleSourceDispatcher,
IPriceOracleSourceDispatcherTrait, PriceWithUpdateTime
};
use crate::libraries::ownable;

#[storage]
struct Storage {
// token -> source
sources: LegacyMap::<ContractAddress, ContractAddress>,
// Unlike in `ZToken`, we don't need to maintain storage compatibility here as this
// contract is not upgradeable.
owner: ContractAddress
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
TokenSourceChanged: TokenSourceChanged,
OwnershipTransferred: OwnershipTransferred,
}

#[derive(Drop, PartialEq, starknet::Event)]
struct TokenSourceChanged {
token: ContractAddress,
source: ContractAddress
}

#[derive(Drop, PartialEq, starknet::Event)]
struct OwnershipTransferred {
previous_owner: ContractAddress,
new_owner: ContractAddress,
}

#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
ownable::initializer(ref self, owner);
}

#[external(v0)]
impl IPriceOracleImpl of IPriceOracle<ContractState> {
fn get_price(self: @ContractState, token: ContractAddress) -> felt252 {
let source = self.sources.read(token);
IPriceOracleSourceDispatcher { contract_address: source }.get_price()
}

fn get_price_with_time(
self: @ContractState, token: ContractAddress
) -> PriceWithUpdateTime {
let source = self.sources.read(token);
IPriceOracleSourceDispatcher { contract_address: source }.get_price_with_time()
}
}

#[external(v0)]
impl IDefaultPriceOracleImpl of IDefaultPriceOracle<ContractState> {
fn set_token_source(
ref self: ContractState, token: ContractAddress, source: ContractAddress
) {
ownable::assert_only_owner(@self);

self.sources.write(token, source);

self.emit(Event::TokenSourceChanged(TokenSourceChanged { token, source }));
}
}

impl DefaultPriceOracleOwnable of ownable::Ownable<ContractState> {
fn read_owner(self: @ContractState) -> ContractAddress {
self.owner.read()
}

fn write_owner(ref self: ContractState, owner: ContractAddress) {
self.owner.write(owner);
}

fn emit_ownership_transferred(
ref self: ContractState, previous_owner: ContractAddress, new_owner: ContractAddress
) {
self
.emit(
Event::OwnershipTransferred(OwnershipTransferred { previous_owner, new_owner })
);
}
}
}

Loading

0 comments on commit 482d14f

Please sign in to comment.