Skip to content

Commit d4e65ed

Browse files
authored
Merge pull request #1 from dewiz-xyz/initial-implemenation
Implementation and tests
2 parents 23b4c3d + 81f1899 commit d4e65ed

File tree

11 files changed

+349
-46
lines changed

11 files changed

+349
-46
lines changed

.github/workflows/test.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ on:
77

88
env:
99
FOUNDRY_PROFILE: ci
10+
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
11+
1012

1113
jobs:
1214
check:
@@ -23,7 +25,7 @@ jobs:
2325
- name: Install Foundry
2426
uses: foundry-rs/foundry-toolchain@v1
2527
with:
26-
version: nightly
28+
version: stable
2729

2830
- name: Show Forge version
2931
run: |

README.md

+30-44
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,52 @@
1-
## Foundry
1+
# dss-blow2
22

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

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

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

12-
## Documentation
9+
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.
1310

14-
https://book.getfoundry.sh/
11+
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).
1512

16-
## Usage
13+
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`):
1714

18-
### Build
19-
20-
```shell
21-
$ forge build
15+
```
16+
surplus_buffer = vat.dai(vow) - vat.vice
2217
```
2318

24-
### Test
2519

26-
```shell
27-
$ forge test
28-
```
20+
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`.
2921

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

32-
```shell
33-
$ forge fmt
34-
```
24+
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.
25+
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.
3526

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

38-
```shell
39-
$ forge snapshot
40-
```
29+
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.
4130

42-
### Anvil
31+
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:
4332

44-
```shell
45-
$ anvil
46-
```
33+
- **Unified Address:** Users can send Dai or USDS to the **same address**—the `DssBlow2` instance.
34+
- **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.
35+
- **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.
4736

48-
### Deploy
37+
## Solution Design
4938

50-
```shell
51-
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
52-
```
39+
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.
5340

54-
### Cast
41+
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.
5542

56-
```shell
57-
$ cast <subcommand>
58-
```
43+
![Architecture Diagram](img/architecture.png)
5944

60-
### Help
45+
## Installation
6146

62-
```shell
63-
$ forge --help
64-
$ anvil --help
65-
$ cast --help
66-
```
47+
To install the project, clone the repository and set up the necessary dependencies:
48+
49+
```bash
50+
git clone https://github.com/dewiz-xyz/dss-blow2.git
51+
cd dss-blow2
52+
forge install

foundry.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ src = "src"
33
out = "out"
44
script = 'script'
55
libs = ["lib"]
6-
solc = '0.8.26'
6+
solc = '0.8.24'
77
optimizer = false
88

99
fs_permissions = [

img/architecture.png

148 KB
Loading

script/DssBlow2Deploy.s.sol

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-FileCopyrightText: 2025 Dai Foundation <www.daifoundation.org>
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
pragma solidity ^0.8.24;
17+
18+
import {Script} from "forge-std/Script.sol";
19+
import {stdJson} from "forge-std/StdJson.sol";
20+
import {MCD, DssInstance} from "dss-test/MCD.sol";
21+
import {ScriptTools} from "dss-test/ScriptTools.sol";
22+
import {DssBlow2Deploy, DssBlow2DeployParams} from "src/deployment/DssBlow2Deploy.sol";
23+
import {DssBlow2Instance} from "src/deployment/DssBlow2Instance.sol";
24+
25+
contract DssBlow2DeployScript is Script {
26+
using stdJson for string;
27+
using ScriptTools for string;
28+
29+
string constant NAME = "dss-blow-2-deploy";
30+
31+
address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
32+
DssInstance dss = MCD.loadFromChainlog(CHAINLOG);
33+
address usdsJoin = dss.chainlog.getAddress("USDS_JOIN");
34+
DssBlow2Instance inst;
35+
36+
function run() external {
37+
vm.startBroadcast();
38+
39+
inst = DssBlow2Deploy.deploy(
40+
DssBlow2DeployParams({daiJoin: address(dss.daiJoin), usdsJoin: usdsJoin, vow: address(dss.vow)})
41+
);
42+
43+
vm.stopBroadcast();
44+
45+
ScriptTools.exportContract(NAME, "blow2", inst.blow);
46+
ScriptTools.exportContract(NAME, "daiJoin", address(dss.daiJoin));
47+
ScriptTools.exportContract(NAME, "usdsJoin", usdsJoin);
48+
ScriptTools.exportContract(NAME, "vow", address(dss.vow));
49+
}
50+
}

script/input/1/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Script inputs for Mainnet.

script/output/1/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Script outputs for Mainnet.

src/DssBlow2.sol

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-FileCopyrightText: 2025 Dai Foundation <www.daifoundation.org>
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
pragma solidity ^0.8.24;
18+
19+
interface ERC20Like {
20+
function balanceOf(address account) external view returns (uint256);
21+
function approve(address usr, uint256 wad) external returns (bool);
22+
}
23+
24+
interface DaiJoinLike {
25+
function dai() external view returns (address);
26+
function join(address usr, uint256 wad) external;
27+
}
28+
29+
interface UsdsJoinLike is DaiJoinLike {
30+
function usds() external view returns (address);
31+
}
32+
33+
/// @title DssBlow2
34+
/// @notice This contract acts as a bridge to incorporate any available Dai or USDS
35+
/// balances into the protocol's Surplus Buffer by invoking the appropriate join adapters.
36+
/// @dev The contract automatically approves the maximum token amount for both join adapters during construction.
37+
contract DssBlow2 {
38+
/// @notice The address of the Vow contract that receives tokens.
39+
address public immutable vow;
40+
41+
/// @notice The ERC20 token representing Dai.
42+
ERC20Like public immutable dai;
43+
44+
/// @notice The ERC20 token representing USDS.
45+
ERC20Like public immutable usds;
46+
47+
/// @notice The adapter for joining Dai into the protocol.
48+
DaiJoinLike public immutable daiJoin;
49+
50+
/// @notice The adapter for joining USDS into the protocol.
51+
UsdsJoinLike public immutable usdsJoin;
52+
53+
/// @notice Emitted when tokens are transferred into the protocol.
54+
/// @param token The address of the token (Dai or USDS) that was transferred.
55+
/// @param amount The amount of tokens that was transferred.
56+
event Blow(address indexed token, uint256 amount);
57+
58+
/// @notice Initializes the DssBlow2 contract.
59+
/// @param daiJoin_ The address of the DaiJoin contract.
60+
/// @param usdsJoin_ The address of the UsdsJoin contract.
61+
/// @param vow_ The address of the Vow contract.
62+
constructor(address daiJoin_, address usdsJoin_, address vow_) {
63+
daiJoin = DaiJoinLike(daiJoin_);
64+
dai = ERC20Like(daiJoin.dai());
65+
usdsJoin = UsdsJoinLike(usdsJoin_);
66+
usds = ERC20Like(usdsJoin.usds());
67+
vow = vow_;
68+
69+
// Approve the maximum uint256 amount for both join adapters.
70+
dai.approve(daiJoin_, type(uint256).max);
71+
usds.approve(usdsJoin_, type(uint256).max);
72+
}
73+
74+
/// @notice Transfers any available Dai and USDS balances from this contract to the protocol's Surplus Buffer.
75+
/// @dev For each token, if the balance is greater than zero, the respective join adapter's join function is called.
76+
function blow() public {
77+
uint256 daiBalance = dai.balanceOf(address(this));
78+
if (daiBalance > 0) {
79+
daiJoin.join(vow, daiBalance);
80+
emit Blow(address(dai), daiBalance);
81+
}
82+
83+
uint256 usdsBalance = usds.balanceOf(address(this));
84+
if (usdsBalance > 0) {
85+
usdsJoin.join(vow, usdsBalance);
86+
emit Blow(address(usds), usdsBalance);
87+
}
88+
}
89+
}

src/DssBlow2.t.sol

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-FileCopyrightText: 2025 Dai Foundation <www.daifoundation.org>
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
pragma solidity ^0.8.24;
17+
18+
import "dss-test/DssTest.sol";
19+
import "./DssBlow2.sol";
20+
21+
contract DssBlow2Test is DssTest {
22+
address constant CHAINLOG = 0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F;
23+
24+
DssInstance dss;
25+
DssBlow2 dssBlow2;
26+
27+
address usds;
28+
address usdsJoin;
29+
address vow;
30+
31+
event Blow(address indexed token, uint256 amount);
32+
33+
function setUp() public {
34+
vm.createSelectFork("mainnet");
35+
// get all the relevant addresses
36+
dss = MCD.loadFromChainlog(CHAINLOG);
37+
usds = dss.chainlog.getAddress("USDS");
38+
usdsJoin = dss.chainlog.getAddress("USDS_JOIN");
39+
vow = address(dss.vow);
40+
41+
dssBlow2 = new DssBlow2(address(dss.daiJoin), usdsJoin, vow);
42+
43+
vm.label(address(dss.dai), "Dai");
44+
vm.label(address(dss.daiJoin), "DaiJoin");
45+
vm.label(usds, "Usds");
46+
vm.label(usdsJoin, "UsdsJoin");
47+
vm.label(address(dss.vow), "Vow");
48+
}
49+
50+
function test_blow() public {
51+
// send dai and usds to DssBlow2
52+
uint256 daiAmount = 10 ether;
53+
uint256 usdsAmount = 5 ether;
54+
deal(address(dss.dai), address(dssBlow2), daiAmount);
55+
deal(usds, address(dssBlow2), usdsAmount);
56+
// store balances before blow()
57+
uint256 vowDaiBalance = dss.vat.dai(vow);
58+
uint256 blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
59+
uint256 blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
60+
assertEq(blowDaiBalance, daiAmount);
61+
assertEq(blowUsdsBalance, usdsAmount);
62+
// event emission
63+
vm.expectEmit(true, false, false, true);
64+
emit Blow(address(dss.dai), daiAmount);
65+
vm.expectEmit(true, false, false, true);
66+
emit Blow(usds, usdsAmount);
67+
// call blow()
68+
dssBlow2.blow();
69+
// check balances after blow()
70+
blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
71+
blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
72+
assertEq(blowDaiBalance, 0);
73+
assertEq(blowUsdsBalance, 0);
74+
// the vat dai balance is in rad so we multiply with ray
75+
assertEq(dss.vat.dai(vow), vowDaiBalance + (daiAmount + usdsAmount) * RAY, "blowDaiUsds: vow balance mismatch");
76+
}
77+
78+
function test_blowDai() public {
79+
// send only dai to DssBlow2
80+
uint256 daiAmount = 10 ether;
81+
deal(address(dss.dai), address(dssBlow2), daiAmount);
82+
// store balances before blow()
83+
uint256 vowDaiBalance = dss.vat.dai(vow);
84+
uint256 blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
85+
uint256 blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
86+
assertEq(blowDaiBalance, daiAmount);
87+
// event emission
88+
vm.expectEmit(true, false, false, true);
89+
emit Blow(address(dss.dai), daiAmount);
90+
// call blow()
91+
dssBlow2.blow();
92+
// check balances after blow()
93+
blowDaiBalance = dss.dai.balanceOf(vow);
94+
blowUsdsBalance = ERC20Like(usds).balanceOf(vow);
95+
assertEq(blowDaiBalance, 0);
96+
assertEq(blowUsdsBalance, 0);
97+
// the vat dai balance is in rad so we multiply with ray
98+
assertEq(dss.vat.dai(vow), vowDaiBalance + daiAmount * RAY, "blowDai: vow balance mismatch");
99+
}
100+
101+
function test_blowUsds() public {
102+
// send only usds to DssBlow2
103+
uint256 usdsAmount = 5 ether;
104+
deal(usds, address(dssBlow2), usdsAmount);
105+
// store balances before blow()
106+
uint256 vowDaiBalance = dss.vat.dai(vow);
107+
uint256 blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
108+
uint256 blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
109+
assertEq(blowUsdsBalance, usdsAmount);
110+
// event emission
111+
vm.expectEmit(true, false, false, true);
112+
emit Blow(usds, usdsAmount);
113+
// call blow()
114+
dssBlow2.blow();
115+
// check balances after blow()
116+
blowDaiBalance = dss.dai.balanceOf(address(dssBlow2));
117+
blowUsdsBalance = ERC20Like(usds).balanceOf(address(dssBlow2));
118+
assertEq(blowDaiBalance, 0);
119+
assertEq(blowUsdsBalance, 0);
120+
// the vat dai balance is in rad so we multiply with ray
121+
assertEq(dss.vat.dai(vow), vowDaiBalance + usdsAmount * RAY, "blowUsds: vow balance mismatch");
122+
}
123+
}

0 commit comments

Comments
 (0)