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 {