Skip to content

Commit

Permalink
Merge pull request #1 from dewiz-xyz/initial-implemenation
Browse files Browse the repository at this point in the history
Implementation and tests
  • Loading branch information
0xp3th1um authored Feb 10, 2025
2 parents 23b4c3d + 81f1899 commit d4e65ed
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 46 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:

env:
FOUNDRY_PROFILE: ci
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}


jobs:
check:
Expand All @@ -23,7 +25,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable

- name: Show Forge version
run: |
Expand Down
74 changes: 30 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,52 @@
## Foundry
# dss-blow2

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
## Description

Foundry consists of:
`dss-blow2` is a smart contract designed to facilitate the refunding of Dai or USDS to the Sky Protocol.

- **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.
## Overview

## Documentation
There are a few actions that are harder than expected when interacting with the Sky Protocol. For instance, returning Dai/USDS to SkyDAO is a non-trivial operation for users who are not familiar with the protocol’s inner workings.

https://book.getfoundry.sh/
From an accounting perspective, any returns made should be considered extra revenue. However, the protocol's revenue is not stored anywhere within the system. Instead, the closest representation is the **Surplus Buffer**, which can be roughly described as a _quasi_-instant snapshot of Sky's accumulated profits. More information on that can be found in [this article](https://hackmd.io/@infomorph/rJ7Wjcrai).

## Usage
Technically speaking, the Surplus Buffer is calculated as the internal balance of the [`Vow`](https://etherscan.io/address/0xa950524441892a31ebddf91d3ceefa04bf454466) (`vat.dai(vow)`) minus the total bad debt in the system (`vat.vice`):

### Build

```shell
$ forge build
```
surplus_buffer = vat.dai(vow) - vat.vice
```

### Test

```shell
$ forge test
```
Thus, the "correct" way to return Dai/USDS to the protocol is by incorporating them into the Surplus Buffer. The downside is that there is no associated address for the Surplus Buffer—it is merely a data entry in the `Vat`.

### Format
Instead of making a standard ERC-20 transfer, anyone wishing to return tokens to the protocol must:

```shell
$ forge fmt
```
1. Call `dai.approve(daiJoin, amt)` or `usds.approve(usdsJoin, amt)` to allow [`daiJoin`](https://etherscan.io/address/0x9759a6ac90977b93b58547b4a71c78317f391a28) or [`usdsJoin`](https://etherscan.io/address/0x3c0f895007ca717aa01c8693e59df1e8c3777feb) to spend Dai/USDS.
2. Call `daiJoin.join(vow, amt)` or `usdsJoin.join(vow, amt)` to burn the ERC-20 Dai/USDS and incorporate it into the Surplus Buffer.

### Gas Snapshots
Both operations can be challenging (if not impossible) due to wallet limitations and tend to be error-prone.

```shell
$ forge snapshot
```
The original [`DssBlow` contract](https://etherscan.io/address/0x0048fc4357db3c0f45adea433a07a20769ddb0cf#code) was created as a standard ERC-20 "bridge" between users and the Sky Protocol. It allows any user to simply transfer Dai to it, and a permissionless function named `blow()` then incorporates the outstanding Dai balance of the contract into the Surplus Buffer.

### Anvil
While `DssBlow` works well for Dai, the same approach would not work for USDS, since the Sky Ecosystem has adopted the new native stablecoin. `DssBlow2` is capable of handling both Dai and USDS simultaneously:

```shell
$ anvil
```
- **Unified Address:** Users can send Dai or USDS to the **same address**—the `DssBlow2` instance.
- **Single Entry Point:** Anyone can call `blow()`, which will add both the outstanding Dai and USDS balances to the Surplus Buffer at the same time.
- **Function Override Consideration:** The `blow(uint256 wad)` override does not make much sense in this context. It should either be removed or modified to `blow(address nat, uint256 wad)` to allow users to specify which native token they wish to use in the transaction.

### Deploy
## Solution Design

```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```
The primary design principle is simplicity. `DssBlow2` will function similarly to `DssBlow`, with the `blow()` function handling the return of both `DAI` and `USDS` to the protocol. Although this function is permissionless, it is expected to be invoked periodically—either by a keeper or directly by a user. As mentioned earlier, there is no need for an override function. Each time `blow()` is called, the contract’s Dai and USDS balances will be checked to determine whether a transfer is necessary. The logic and frequency of invocation will be managed at the keeper level, especially if a cron job is implemented.

### Cast
We decided to pass the necessary addresses (i.e., `daiJoin`, `usdsJoin`, and `vow`) as constructor arguments. The `dai` and `usds` ERC-20 contracts are obtained via their corresponding joins. In the rare event that one of the relevant addresses changes, the contract would need to be redeployed.

```shell
$ cast <subcommand>
```
![Architecture Diagram](img/architecture.png)

### Help
## Installation

```shell
$ forge --help
$ anvil --help
$ cast --help
```
To install the project, clone the repository and set up the necessary dependencies:

```bash
git clone https://github.com/dewiz-xyz/dss-blow2.git
cd dss-blow2
forge install
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ src = "src"
out = "out"
script = 'script'
libs = ["lib"]
solc = '0.8.26'
solc = '0.8.24'
optimizer = false

fs_permissions = [
Expand Down
Binary file added img/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions script/DssBlow2Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2025 Dai Foundation <www.daifoundation.org>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pragma solidity ^0.8.24;

import {Script} from "forge-std/Script.sol";
import {stdJson} from "forge-std/StdJson.sol";
import {MCD, DssInstance} from "dss-test/MCD.sol";
import {ScriptTools} from "dss-test/ScriptTools.sol";
import {DssBlow2Deploy, DssBlow2DeployParams} from "src/deployment/DssBlow2Deploy.sol";
import {DssBlow2Instance} from "src/deployment/DssBlow2Instance.sol";

contract DssBlow2DeployScript is Script {
using stdJson for string;
using ScriptTools for string;

string constant NAME = "dss-blow-2-deploy";

address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
DssInstance dss = MCD.loadFromChainlog(CHAINLOG);
address usdsJoin = dss.chainlog.getAddress("USDS_JOIN");
DssBlow2Instance inst;

function run() external {
vm.startBroadcast();

inst = DssBlow2Deploy.deploy(
DssBlow2DeployParams({daiJoin: address(dss.daiJoin), usdsJoin: usdsJoin, vow: address(dss.vow)})
);

vm.stopBroadcast();

ScriptTools.exportContract(NAME, "blow2", inst.blow);
ScriptTools.exportContract(NAME, "daiJoin", address(dss.daiJoin));
ScriptTools.exportContract(NAME, "usdsJoin", usdsJoin);
ScriptTools.exportContract(NAME, "vow", address(dss.vow));
}
}
1 change: 1 addition & 0 deletions script/input/1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Script inputs for Mainnet.
1 change: 1 addition & 0 deletions script/output/1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Script outputs for Mainnet.
89 changes: 89 additions & 0 deletions src/DssBlow2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2025 Dai Foundation <www.daifoundation.org>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

pragma solidity ^0.8.24;

interface ERC20Like {
function balanceOf(address account) external view returns (uint256);
function approve(address usr, uint256 wad) external returns (bool);
}

interface DaiJoinLike {
function dai() external view returns (address);
function join(address usr, uint256 wad) external;
}

interface UsdsJoinLike is DaiJoinLike {
function usds() external view returns (address);
}

/// @title DssBlow2
/// @notice This contract acts as a bridge to incorporate any available Dai or USDS
/// balances into the protocol's Surplus Buffer by invoking the appropriate join adapters.
/// @dev The contract automatically approves the maximum token amount for both join adapters during construction.
contract DssBlow2 {
/// @notice The address of the Vow contract that receives tokens.
address public immutable vow;

/// @notice The ERC20 token representing Dai.
ERC20Like public immutable dai;

/// @notice The ERC20 token representing USDS.
ERC20Like public immutable usds;

/// @notice The adapter for joining Dai into the protocol.
DaiJoinLike public immutable daiJoin;

/// @notice The adapter for joining USDS into the protocol.
UsdsJoinLike public immutable usdsJoin;

/// @notice Emitted when tokens are transferred into the protocol.
/// @param token The address of the token (Dai or USDS) that was transferred.
/// @param amount The amount of tokens that was transferred.
event Blow(address indexed token, uint256 amount);

/// @notice Initializes the DssBlow2 contract.
/// @param daiJoin_ The address of the DaiJoin contract.
/// @param usdsJoin_ The address of the UsdsJoin contract.
/// @param vow_ The address of the Vow contract.
constructor(address daiJoin_, address usdsJoin_, address vow_) {
daiJoin = DaiJoinLike(daiJoin_);
dai = ERC20Like(daiJoin.dai());
usdsJoin = UsdsJoinLike(usdsJoin_);
usds = ERC20Like(usdsJoin.usds());
vow = vow_;

// Approve the maximum uint256 amount for both join adapters.
dai.approve(daiJoin_, type(uint256).max);
usds.approve(usdsJoin_, type(uint256).max);
}

/// @notice Transfers any available Dai and USDS balances from this contract to the protocol's Surplus Buffer.
/// @dev For each token, if the balance is greater than zero, the respective join adapter's join function is called.
function blow() public {
uint256 daiBalance = dai.balanceOf(address(this));
if (daiBalance > 0) {
daiJoin.join(vow, daiBalance);
emit Blow(address(dai), daiBalance);
}

uint256 usdsBalance = usds.balanceOf(address(this));
if (usdsBalance > 0) {
usdsJoin.join(vow, usdsBalance);
emit Blow(address(usds), usdsBalance);
}
}
}
123 changes: 123 additions & 0 deletions src/DssBlow2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2025 Dai Foundation <www.daifoundation.org>
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pragma solidity ^0.8.24;

import "dss-test/DssTest.sol";
import "./DssBlow2.sol";

contract DssBlow2Test is DssTest {
address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;

DssInstance dss;
DssBlow2 dssBlow2;

address usds;
address usdsJoin;
address vow;

event Blow(address indexed token, uint256 amount);

function setUp() public {
vm.createSelectFork("mainnet");
// get all the relevant addresses
dss = MCD.loadFromChainlog(CHAINLOG);
usds = dss.chainlog.getAddress("USDS");
usdsJoin = dss.chainlog.getAddress("USDS_JOIN");
vow = address(dss.vow);

dssBlow2 = new DssBlow2(address(dss.daiJoin), usdsJoin, vow);

vm.label(address(dss.dai), "Dai");
vm.label(address(dss.daiJoin), "DaiJoin");
vm.label(usds, "Usds");
vm.label(usdsJoin, "UsdsJoin");
vm.label(address(dss.vow), "Vow");
}

function test_blow() public {
// send dai and usds to DssBlow2
uint256 daiAmount = 10 ether;
uint256 usdsAmount = 5 ether;
deal(address(dss.dai), address(dssBlow2), daiAmount);
deal(usds, address(dssBlow2), usdsAmount);
// store balances before blow()
uint256 vowDaiBalance = dss.vat.dai(vow);
uint256 blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
uint256 blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
assertEq(blowDaiBalance, daiAmount);
assertEq(blowUsdsBalance, usdsAmount);
// event emission
vm.expectEmit(true, false, false, true);
emit Blow(address(dss.dai), daiAmount);
vm.expectEmit(true, false, false, true);
emit Blow(usds, usdsAmount);
// call blow()
dssBlow2.blow();
// check balances after blow()
blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
assertEq(blowDaiBalance, 0);
assertEq(blowUsdsBalance, 0);
// the vat dai balance is in rad so we multiply with ray
assertEq(dss.vat.dai(vow), vowDaiBalance + (daiAmount + usdsAmount) * RAY, "blowDaiUsds: vow balance mismatch");
}

function test_blowDai() public {
// send only dai to DssBlow2
uint256 daiAmount = 10 ether;
deal(address(dss.dai), address(dssBlow2), daiAmount);
// store balances before blow()
uint256 vowDaiBalance = dss.vat.dai(vow);
uint256 blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
uint256 blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
assertEq(blowDaiBalance, daiAmount);
// event emission
vm.expectEmit(true, false, false, true);
emit Blow(address(dss.dai), daiAmount);
// call blow()
dssBlow2.blow();
// check balances after blow()
blowDaiBalance = dss.dai.balanceOf(vow);
blowUsdsBalance = ERC20Like(usds).balanceOf(vow);
assertEq(blowDaiBalance, 0);
assertEq(blowUsdsBalance, 0);
// the vat dai balance is in rad so we multiply with ray
assertEq(dss.vat.dai(vow), vowDaiBalance + daiAmount * RAY, "blowDai: vow balance mismatch");
}

function test_blowUsds() public {
// send only usds to DssBlow2
uint256 usdsAmount = 5 ether;
deal(usds, address(dssBlow2), usdsAmount);
// store balances before blow()
uint256 vowDaiBalance = dss.vat.dai(vow);
uint256 blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
uint256 blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
assertEq(blowUsdsBalance, usdsAmount);
// event emission
vm.expectEmit(true, false, false, true);
emit Blow(usds, usdsAmount);
// call blow()
dssBlow2.blow();
// check balances after blow()
blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
assertEq(blowDaiBalance, 0);
assertEq(blowUsdsBalance, 0);
// the vat dai balance is in rad so we multiply with ray
assertEq(dss.vat.dai(vow), vowDaiBalance + usdsAmount * RAY, "blowUsds: vow balance mismatch");
}
}
Loading

0 comments on commit d4e65ed

Please sign in to comment.