From 6b6a887579cf59cdc7305cb54c9446003f0d5d71 Mon Sep 17 00:00:00 2001
From: oldchili <130549691+oldchili@users.noreply.github.com>
Date: Tue, 19 Nov 2024 15:19:32 +0200
Subject: [PATCH 1/3] Supply sync implemenation
---
README.md | 4 ++
deploy/SkyDeploy.sol | 9 +++
deploy/SkyInit.sol | 20 ++++++
src/SupplySync.sol | 54 +++++++++++++++
test/integration/SupplySync.t.sol | 106 ++++++++++++++++++++++++++++++
5 files changed, 193 insertions(+)
create mode 100644 src/SupplySync.sol
create mode 100644 test/integration/SupplySync.t.sol
diff --git a/README.md b/README.md
index 642bb70..f1acc47 100644
--- a/README.md
+++ b/README.md
@@ -17,3 +17,7 @@ It is a converter between `Mkr` and `Sky` (both ways). Using the `mint` and `bur
**Note:** if one of the tokens removes `mint` capabilities to this contract, it means that the path which gives that token to the user won't be available.
**Note 2:** In the MKR -> SKY conversion, if the user passes a `wad` amount not multiple of `rate`, it causes that a dusty value will be lost.
+
+### SupplySync
+
+A contract with permissionless functionality that syncs the SKY supply to include also the MKR supply (thus MKR acts as wrapper of SKY).
diff --git a/deploy/SkyDeploy.sol b/deploy/SkyDeploy.sol
index 96da375..e08f751 100644
--- a/deploy/SkyDeploy.sol
+++ b/deploy/SkyDeploy.sol
@@ -20,6 +20,7 @@ import { ScriptTools } from "dss-test/ScriptTools.sol";
import { Sky } from "src/Sky.sol";
import { MkrSky } from "src/MkrSky.sol";
+import { SupplySync } from "src/SupplySync.sol";
import { SkyInstance } from "./SkyInstance.sol";
@@ -46,4 +47,12 @@ library SkyDeploy {
sky = address(new Sky());
ScriptTools.switchOwner(sky, deployer, owner);
}
+
+ function deploySupplySync(
+ address mkr,
+ address sky,
+ address owner
+ ) internal returns (address supplySync) {
+ supplySync = address(new SupplySync(mkr, sky, owner));
+ }
}
diff --git a/deploy/SkyInit.sol b/deploy/SkyInit.sol
index 90dae83..3207ba0 100644
--- a/deploy/SkyInit.sol
+++ b/deploy/SkyInit.sol
@@ -21,6 +21,7 @@ import { SkyInstance } from "./SkyInstance.sol";
interface SkyLike {
function rely(address) external;
+ function allowance(address, address) external view returns (uint256);
}
interface MkrSkyLike {
@@ -29,6 +30,11 @@ interface MkrSkyLike {
function rate() external view returns (uint256);
}
+interface SupplySyncLike {
+ function mkr() external view returns (address);
+ function sky() external view returns (address);
+}
+
interface MkrLike {
function authority() external view returns (address);
}
@@ -54,4 +60,18 @@ library SkyInit {
dss.chainlog.setAddress("SKY", instance.sky);
dss.chainlog.setAddress("MKR_SKY", instance.mkrSky);
}
+
+ function initSupplySync(
+ DssInstance memory dss,
+ address supplySync
+ ) internal {
+ SkyLike sky = SkyLike(dss.chainlog.getAddress("SKY"));
+
+ require(SupplySyncLike(supplySync).mkr() == dss.chainlog.getAddress("MCD_GOV"), "SkyInit/mkr-does-not-match");
+ require(SupplySyncLike(supplySync).sky() == address(sky), "SkyInit/sky-does-not-match");
+ require(sky.allowance(supplySync, dss.chainlog.getAddress("MCD_PAUSE_PROXY")) == type(uint256).max, "SkyInit/allowance-not-set");
+
+ sky.rely(supplySync);
+ dss.chainlog.setAddress("SKY_SUPPLY_SYNC", supplySync);
+ }
}
diff --git a/src/SupplySync.sol b/src/SupplySync.sol
new file mode 100644
index 0000000..5d93a35
--- /dev/null
+++ b/src/SupplySync.sol
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/// MkrSky.sol -- Mkr/Sky Exchanger
+
+// Copyright (C) 2023 Dai Foundation
+//
+// 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 .
+
+pragma solidity ^0.8.21;
+
+interface GemLike {
+ function totalSupply() external view returns (uint256);
+ function balanceOf(address) external view returns (uint256);
+ function approve(address, uint256) external;
+ function mint(address, uint256) external;
+ function burn(address, uint256) external;
+}
+
+contract SupplySync {
+ GemLike public immutable mkr;
+ GemLike public immutable sky;
+
+ constructor(address mkr_, address sky_, address owner) {
+ mkr = GemLike(mkr_);
+ sky = GemLike(sky_);
+
+ // Allow owner (pause proxy) to burn the sky in this contract, if ever needed to wind down
+ sky.approve(owner, type(uint256).max);
+ }
+
+ function sync() external {
+ uint256 mkrSupplyInSky = mkr.totalSupply() * 24_000;
+ uint256 skyBalance = sky.balanceOf(address(this));
+
+ unchecked {
+ if (mkrSupplyInSky > skyBalance) {
+ sky.mint(address(this), mkrSupplyInSky - skyBalance);
+ } else {
+ sky.burn(address(this), skyBalance - mkrSupplyInSky);
+ }
+ }
+ }
+}
diff --git a/test/integration/SupplySync.t.sol b/test/integration/SupplySync.t.sol
new file mode 100644
index 0000000..b30273a
--- /dev/null
+++ b/test/integration/SupplySync.t.sol
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+pragma solidity ^0.8.21;
+
+import "dss-test/DssTest.sol";
+import { SupplySync } from "src/SupplySync.sol";
+import { SkyDeploy } from "deploy/SkyDeploy.sol";
+import { SkyInit } from "deploy/SkyInit.sol";
+
+interface GemLike {
+ function totalSupply() external view returns (uint256);
+ function balanceOf(address) external view returns (uint256);
+ function allowance(address, address) external view returns (uint256);
+ function burn(address, uint256) external;
+}
+
+interface SkyLike is GemLike {
+ function wards(address) external view returns (uint256);
+ function rely(address) external;
+ function deny(address) external;
+}
+
+contract SupplySyncTest is DssTest {
+ DssInstance dss;
+
+ address PAUSE_PROXY;
+ GemLike MKR;
+ SkyLike SKY;
+
+ SupplySync sync;
+
+ function setUp() public {
+ vm.createSelectFork(vm.envString("ETH_RPC_URL"));
+
+ dss = MCD.loadFromChainlog(0xdA0Ab1e0017DEbCd72Be8599041a2aa3bA7e740F);
+
+ PAUSE_PROXY = dss.chainlog.getAddress("MCD_PAUSE_PROXY");
+ MKR = GemLike(dss.chainlog.getAddress("MCD_GOV"));
+ SKY = SkyLike(dss.chainlog.getAddress("SKY"));
+
+ sync = SupplySync(SkyDeploy.deploySupplySync(address(MKR), address(SKY), PAUSE_PROXY));
+ vm.startPrank(PAUSE_PROXY);
+ SkyInit.initSupplySync(dss, address(sync));
+ vm.stopPrank();
+ }
+
+ function testDeployAndInit() public {
+ assertEq(address(sync.mkr()), address(MKR));
+ assertEq(address(sync.sky()), address(SKY));
+ assertEq(SKY.allowance(address(sync), PAUSE_PROXY), type(uint256).max);
+ assertEq(SKY.wards(address(sync)), 1);
+ assertEq(dss.chainlog.getAddress("SKY_SUPPLY_SYNC"), address(sync));
+ }
+
+ function _checkSync(bool isExpectedMint, uint256 expectedChange) internal {
+ uint256 mkrSupply = MKR.totalSupply();
+ uint256 skySupplyBefore = SKY.totalSupply();
+ uint256 syncBalanceBefore = SKY.balanceOf(address(sync));
+
+ sync.sync();
+
+ uint256 syncBalanceAfter = SKY.balanceOf(address(sync));
+
+ assertEq(syncBalanceAfter, mkrSupply * 24_000);
+ if (isExpectedMint) {
+ assertEq(syncBalanceAfter, syncBalanceBefore + expectedChange);
+ assertEq(SKY.totalSupply(), skySupplyBefore + expectedChange);
+ } else {
+ assertEq(syncBalanceAfter, syncBalanceBefore - expectedChange);
+ assertEq(SKY.totalSupply(), skySupplyBefore - expectedChange);
+ }
+ }
+
+ function testSZeroSkyInSync() public {
+ deal(address(SKY), address(sync), 0);
+ _checkSync(true, MKR.totalSupply() * 24_000);
+ }
+
+ function testLessSkyInSync() public {
+ deal(address(SKY), address(sync), MKR.totalSupply() * 24_000 - 1234);
+ _checkSync(true, 1234);
+ }
+
+ function testMoreSkyInSync() public {
+ deal(address(SKY), address(sync), MKR.totalSupply() * 24_000 + 1234);
+ _checkSync(false, 1234);
+ }
+
+ function testExactSkyInSync() public {
+ deal(address(SKY), address(sync), MKR.totalSupply() * 24_000);
+ _checkSync(true, 0);
+ }
+
+ function testWindDown() public {
+ deal(address(SKY), address(sync), 1234);
+
+ vm.startPrank(PAUSE_PROXY);
+ SKY.burn(address(sync), SKY.balanceOf(address(sync)));
+ SKY.deny(address(sync)); // revoke mint allowance
+ vm.stopPrank();
+
+ assertEq(SKY.balanceOf(address(sync)), 0);
+ vm.expectRevert("Sky/not-authorized");
+ sync.sync();
+ }
+}
From b0902e893d0192d0a50c16e1c6283e06584fadba Mon Sep 17 00:00:00 2001
From: oldchili <130549691+oldchili@users.noreply.github.com>
Date: Wed, 20 Nov 2024 13:37:21 +0200
Subject: [PATCH 2/3] Handle review comments
---
deploy/SkyDeploy.sol | 5 ++---
deploy/SkyInit.sol | 6 ++++--
src/SupplySync.sol | 18 +++++++++++++-----
test/integration/SupplySync.t.sol | 20 +++++++++++++++++---
4 files changed, 36 insertions(+), 13 deletions(-)
diff --git a/deploy/SkyDeploy.sol b/deploy/SkyDeploy.sol
index e08f751..bc60fe8 100644
--- a/deploy/SkyDeploy.sol
+++ b/deploy/SkyDeploy.sol
@@ -49,10 +49,9 @@ library SkyDeploy {
}
function deploySupplySync(
- address mkr,
- address sky,
+ address mkrSky,
address owner
) internal returns (address supplySync) {
- supplySync = address(new SupplySync(mkr, sky, owner));
+ supplySync = address(new SupplySync(mkrSky, owner));
}
}
diff --git a/deploy/SkyInit.sol b/deploy/SkyInit.sol
index 3207ba0..3cfadb5 100644
--- a/deploy/SkyInit.sol
+++ b/deploy/SkyInit.sol
@@ -33,6 +33,7 @@ interface MkrSkyLike {
interface SupplySyncLike {
function mkr() external view returns (address);
function sky() external view returns (address);
+ function rate() external view returns (uint256);
}
interface MkrLike {
@@ -67,8 +68,9 @@ library SkyInit {
) internal {
SkyLike sky = SkyLike(dss.chainlog.getAddress("SKY"));
- require(SupplySyncLike(supplySync).mkr() == dss.chainlog.getAddress("MCD_GOV"), "SkyInit/mkr-does-not-match");
- require(SupplySyncLike(supplySync).sky() == address(sky), "SkyInit/sky-does-not-match");
+ require(SupplySyncLike(supplySync).mkr() == dss.chainlog.getAddress("MCD_GOV"), "SkyInit/mkr-does-not-match");
+ require(SupplySyncLike(supplySync).sky() == address(sky), "SkyInit/sky-does-not-match");
+ require(SupplySyncLike(supplySync).rate() == 24_000, "SkyInit/rate-does-not-match");
require(sky.allowance(supplySync, dss.chainlog.getAddress("MCD_PAUSE_PROXY")) == type(uint256).max, "SkyInit/allowance-not-set");
sky.rely(supplySync);
diff --git a/src/SupplySync.sol b/src/SupplySync.sol
index 5d93a35..4ba5466 100644
--- a/src/SupplySync.sol
+++ b/src/SupplySync.sol
@@ -2,7 +2,7 @@
/// MkrSky.sol -- Mkr/Sky Exchanger
-// Copyright (C) 2023 Dai Foundation
+// Copyright (C) 2024 Dai Foundation
//
// 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
@@ -27,20 +27,28 @@ interface GemLike {
function burn(address, uint256) external;
}
+interface MkrSkyLike {
+ function mkr() external view returns (address);
+ function sky() external view returns (address);
+ function rate() external view returns (uint256);
+}
+
contract SupplySync {
GemLike public immutable mkr;
GemLike public immutable sky;
+ uint256 public immutable rate;
- constructor(address mkr_, address sky_, address owner) {
- mkr = GemLike(mkr_);
- sky = GemLike(sky_);
+ constructor(address mkrSky, address owner) {
+ mkr = GemLike(MkrSkyLike(mkrSky).mkr());
+ sky = GemLike(MkrSkyLike(mkrSky).sky());
+ rate = MkrSkyLike(mkrSky).rate();
// Allow owner (pause proxy) to burn the sky in this contract, if ever needed to wind down
sky.approve(owner, type(uint256).max);
}
function sync() external {
- uint256 mkrSupplyInSky = mkr.totalSupply() * 24_000;
+ uint256 mkrSupplyInSky = mkr.totalSupply() * rate;
uint256 skyBalance = sky.balanceOf(address(this));
unchecked {
diff --git a/test/integration/SupplySync.t.sol b/test/integration/SupplySync.t.sol
index b30273a..675c6c1 100644
--- a/test/integration/SupplySync.t.sol
+++ b/test/integration/SupplySync.t.sol
@@ -1,4 +1,18 @@
+// SPDX-FileCopyrightText: © 2024 Dai Foundation
// 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 .
pragma solidity ^0.8.21;
@@ -16,7 +30,6 @@ interface GemLike {
interface SkyLike is GemLike {
function wards(address) external view returns (uint256);
- function rely(address) external;
function deny(address) external;
}
@@ -38,7 +51,7 @@ contract SupplySyncTest is DssTest {
MKR = GemLike(dss.chainlog.getAddress("MCD_GOV"));
SKY = SkyLike(dss.chainlog.getAddress("SKY"));
- sync = SupplySync(SkyDeploy.deploySupplySync(address(MKR), address(SKY), PAUSE_PROXY));
+ sync = SupplySync(SkyDeploy.deploySupplySync(dss.chainlog.getAddress("MKR_SKY"), PAUSE_PROXY));
vm.startPrank(PAUSE_PROXY);
SkyInit.initSupplySync(dss, address(sync));
vm.stopPrank();
@@ -47,6 +60,7 @@ contract SupplySyncTest is DssTest {
function testDeployAndInit() public {
assertEq(address(sync.mkr()), address(MKR));
assertEq(address(sync.sky()), address(SKY));
+ assertEq(sync.rate(), 24_000);
assertEq(SKY.allowance(address(sync), PAUSE_PROXY), type(uint256).max);
assertEq(SKY.wards(address(sync)), 1);
assertEq(dss.chainlog.getAddress("SKY_SUPPLY_SYNC"), address(sync));
@@ -71,7 +85,7 @@ contract SupplySyncTest is DssTest {
}
}
- function testSZeroSkyInSync() public {
+ function testZeroSkyInSync() public {
deal(address(SKY), address(sync), 0);
_checkSync(true, MKR.totalSupply() * 24_000);
}
From 1a6eb1ca8e4df785102544c73c92f6df85e65500 Mon Sep 17 00:00:00 2001
From: oldchili <130549691+oldchili@users.noreply.github.com>
Date: Wed, 20 Nov 2024 13:55:57 +0200
Subject: [PATCH 3/3] Change to else if and return values
---
src/SupplySync.sol | 11 +++++++----
test/integration/SupplySync.t.sol | 6 ++++--
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/SupplySync.sol b/src/SupplySync.sol
index 4ba5466..df56a1e 100644
--- a/src/SupplySync.sol
+++ b/src/SupplySync.sol
@@ -47,15 +47,18 @@ contract SupplySync {
sky.approve(owner, type(uint256).max);
}
- function sync() external {
+ function sync() external returns (bool isMint, uint256 amount) {
uint256 mkrSupplyInSky = mkr.totalSupply() * rate;
uint256 skyBalance = sky.balanceOf(address(this));
unchecked {
if (mkrSupplyInSky > skyBalance) {
- sky.mint(address(this), mkrSupplyInSky - skyBalance);
- } else {
- sky.burn(address(this), skyBalance - mkrSupplyInSky);
+ isMint = true;
+ amount = mkrSupplyInSky - skyBalance;
+ sky.mint(address(this), amount);
+ } else if (mkrSupplyInSky < skyBalance) {
+ amount = skyBalance - mkrSupplyInSky;
+ sky.burn(address(this), amount);
}
}
}
diff --git a/test/integration/SupplySync.t.sol b/test/integration/SupplySync.t.sol
index 675c6c1..06448fa 100644
--- a/test/integration/SupplySync.t.sol
+++ b/test/integration/SupplySync.t.sol
@@ -71,11 +71,13 @@ contract SupplySyncTest is DssTest {
uint256 skySupplyBefore = SKY.totalSupply();
uint256 syncBalanceBefore = SKY.balanceOf(address(sync));
- sync.sync();
+ (bool isMint, uint256 amount) = sync.sync();
uint256 syncBalanceAfter = SKY.balanceOf(address(sync));
assertEq(syncBalanceAfter, mkrSupply * 24_000);
+ assertEq(isMint, isExpectedMint);
+ assertEq(amount, expectedChange);
if (isExpectedMint) {
assertEq(syncBalanceAfter, syncBalanceBefore + expectedChange);
assertEq(SKY.totalSupply(), skySupplyBefore + expectedChange);
@@ -102,7 +104,7 @@ contract SupplySyncTest is DssTest {
function testExactSkyInSync() public {
deal(address(SKY), address(sync), MKR.totalSupply() * 24_000);
- _checkSync(true, 0);
+ _checkSync(false, 0);
}
function testWindDown() public {