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..bc60fe8 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,11 @@ library SkyDeploy {
sky = address(new Sky());
ScriptTools.switchOwner(sky, deployer, owner);
}
+
+ function deploySupplySync(
+ address mkrSky,
+ address owner
+ ) internal returns (address supplySync) {
+ supplySync = address(new SupplySync(mkrSky, owner));
+ }
}
diff --git a/deploy/SkyInit.sol b/deploy/SkyInit.sol
index 90dae83..3cfadb5 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,12 @@ interface MkrSkyLike {
function rate() external view returns (uint256);
}
+interface SupplySyncLike {
+ function mkr() external view returns (address);
+ function sky() external view returns (address);
+ function rate() external view returns (uint256);
+}
+
interface MkrLike {
function authority() external view returns (address);
}
@@ -54,4 +61,19 @@ 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(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);
+ dss.chainlog.setAddress("SKY_SUPPLY_SYNC", supplySync);
+ }
}
diff --git a/src/SupplySync.sol b/src/SupplySync.sol
new file mode 100644
index 0000000..df56a1e
--- /dev/null
+++ b/src/SupplySync.sol
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+/// MkrSky.sol -- Mkr/Sky Exchanger
+
+// 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
+// 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;
+}
+
+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 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 returns (bool isMint, uint256 amount) {
+ uint256 mkrSupplyInSky = mkr.totalSupply() * rate;
+ uint256 skyBalance = sky.balanceOf(address(this));
+
+ unchecked {
+ if (mkrSupplyInSky > skyBalance) {
+ 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
new file mode 100644
index 0000000..06448fa
--- /dev/null
+++ b/test/integration/SupplySync.t.sol
@@ -0,0 +1,122 @@
+// 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;
+
+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 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(dss.chainlog.getAddress("MKR_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(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));
+ }
+
+ function _checkSync(bool isExpectedMint, uint256 expectedChange) internal {
+ uint256 mkrSupply = MKR.totalSupply();
+ uint256 skySupplyBefore = SKY.totalSupply();
+ uint256 syncBalanceBefore = SKY.balanceOf(address(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);
+ } else {
+ assertEq(syncBalanceAfter, syncBalanceBefore - expectedChange);
+ assertEq(SKY.totalSupply(), skySupplyBefore - expectedChange);
+ }
+ }
+
+ function testZeroSkyInSync() 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(false, 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();
+ }
+}