-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from dewiz-xyz/initial-implemenation
Implementation and tests
- Loading branch information
Showing
11 changed files
with
349 additions
and
46 deletions.
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
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,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> | ||
``` | ||
 | ||
|
||
### 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 |
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,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)); | ||
} | ||
} |
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 @@ | ||
Script inputs for Mainnet. |
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 @@ | ||
Script outputs for Mainnet. |
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,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); | ||
} | ||
} | ||
} |
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,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"); | ||
} | ||
} |
Oops, something went wrong.