Skip to content

Commit 654c384

Browse files
authored
SIP-2059: Legacy Spot Synth Migration (Synthetixio#2227)
1 parent 4413331 commit 654c384

17 files changed

+921
-96
lines changed

contracts/BaseDebtCache.sol

+12-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import "./interfaces/ICollateralManager.sol";
1919
import "./interfaces/IEtherWrapper.sol";
2020
import "./interfaces/IWrapperFactory.sol";
2121
import "./interfaces/IFuturesMarketManager.sol";
22+
import "./interfaces/IDynamicSynthRedeemer.sol";
2223

2324
// https://docs.synthetix.io/contracts/source/contracts/debtcache
2425
contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
@@ -49,14 +50,15 @@ contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
4950
bytes32 private constant CONTRACT_ETHER_WRAPPER = "EtherWrapper";
5051
bytes32 private constant CONTRACT_FUTURESMARKETMANAGER = "FuturesMarketManager";
5152
bytes32 private constant CONTRACT_WRAPPER_FACTORY = "WrapperFactory";
53+
bytes32 private constant CONTRACT_DYNAMICSYNTHREDEEMER = "DynamicSynthRedeemer";
5254

5355
constructor(address _owner, address _resolver) public Owned(_owner) MixinSystemSettings(_resolver) {}
5456

5557
/* ========== VIEWS ========== */
5658

5759
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
5860
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
59-
bytes32[] memory newAddresses = new bytes32[](8);
61+
bytes32[] memory newAddresses = new bytes32[](9);
6062
newAddresses[0] = CONTRACT_ISSUER;
6163
newAddresses[1] = CONTRACT_EXCHANGER;
6264
newAddresses[2] = CONTRACT_EXRATES;
@@ -65,6 +67,7 @@ contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
6567
newAddresses[5] = CONTRACT_WRAPPER_FACTORY;
6668
newAddresses[6] = CONTRACT_ETHER_WRAPPER;
6769
newAddresses[7] = CONTRACT_FUTURESMARKETMANAGER;
70+
newAddresses[8] = CONTRACT_DYNAMICSYNTHREDEEMER;
6871
addresses = combineArrays(existingAddresses, newAddresses);
6972
}
7073

@@ -100,6 +103,10 @@ contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
100103
return IWrapperFactory(requireAndGetAddress(CONTRACT_WRAPPER_FACTORY));
101104
}
102105

106+
function dynamicSynthRedeemer() internal view returns (IDynamicSynthRedeemer) {
107+
return IDynamicSynthRedeemer(requireAndGetAddress(CONTRACT_DYNAMICSYNTHREDEEMER));
108+
}
109+
103110
function debtSnapshotStaleTime() external view returns (uint) {
104111
return getDebtSnapshotStaleTime();
105112
}
@@ -139,12 +146,15 @@ contract BaseDebtCache is Owned, MixinSystemSettings, IDebtCache {
139146
uint numValues = currencyKeys.length;
140147
values = new uint[](numValues);
141148
ISynth[] memory synths = issuer().getSynths(currencyKeys);
149+
uint discountRate = dynamicSynthRedeemer().getDiscountRate();
142150

143151
for (uint i = 0; i < numValues; i++) {
144152
address synthAddress = address(synths[i]);
145153
require(synthAddress != address(0), "Synth does not exist");
146154
uint supply = IERC20(synthAddress).totalSupply();
147-
values[i] = supply.multiplyDecimalRound(rates[i]);
155+
uint value = supply.multiplyDecimalRound(rates[i]);
156+
uint multiplier = (synths[i].currencyKey() != sUSD) ? discountRate : SafeDecimalMath.unit();
157+
values[i] = value.multiplyDecimalRound(multiplier);
148158
}
149159

150160
return (values);

contracts/DynamicSynthRedeemer.sol

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
pragma solidity ^0.5.16;
2+
3+
// Inheritence
4+
import "./Owned.sol";
5+
import "./MixinResolver.sol";
6+
import "./interfaces/IDynamicSynthRedeemer.sol";
7+
8+
// Libraries
9+
import "./SafeDecimalMath.sol";
10+
11+
// Internal references
12+
import "./interfaces/IIssuer.sol";
13+
import "./interfaces/IExchangeRates.sol";
14+
import "./interfaces/ISynth.sol";
15+
16+
interface IProxy {
17+
function target() external view returns (address);
18+
}
19+
20+
contract DynamicSynthRedeemer is Owned, IDynamicSynthRedeemer, MixinResolver {
21+
using SafeDecimalMath for uint;
22+
23+
bytes32 public constant CONTRACT_NAME = "DynamicSynthRedeemer";
24+
25+
uint public discountRate;
26+
bool public redemptionActive;
27+
28+
bytes32 internal constant sUSD = "sUSD";
29+
30+
bytes32 private constant CONTRACT_ISSUER = "Issuer";
31+
bytes32 private constant CONTRACT_EXRATES = "ExchangeRates";
32+
33+
constructor(address _owner, address _resolver) public Owned(_owner) MixinResolver(_resolver) {
34+
discountRate = SafeDecimalMath.unit();
35+
}
36+
37+
/* ========== RESOLVER CONFIG ========== */
38+
39+
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
40+
addresses = new bytes32[](2);
41+
addresses[0] = CONTRACT_ISSUER;
42+
addresses[1] = CONTRACT_EXRATES;
43+
}
44+
45+
function issuer() internal view returns (IIssuer) {
46+
return IIssuer(requireAndGetAddress(CONTRACT_ISSUER));
47+
}
48+
49+
function exchangeRates() internal view returns (IExchangeRates) {
50+
return IExchangeRates(requireAndGetAddress(CONTRACT_EXRATES));
51+
}
52+
53+
function redeemingActive() internal view {
54+
require(redemptionActive, "Redemption deactivated");
55+
}
56+
57+
/* ========== VIEWS ========== */
58+
59+
function getDiscountRate() external view returns (uint) {
60+
return discountRate;
61+
}
62+
63+
/* ========== MUTATIVE FUNCTIONS ========== */
64+
65+
function redeemAll(address[] calldata synthProxies) external requireRedemptionActive {
66+
for (uint i = 0; i < synthProxies.length; i++) {
67+
_redeem(synthProxies[i], IERC20(synthProxies[i]).balanceOf(msg.sender));
68+
}
69+
}
70+
71+
function redeem(address synthProxy) external requireRedemptionActive {
72+
_redeem(synthProxy, IERC20(synthProxy).balanceOf(msg.sender));
73+
}
74+
75+
function redeemPartial(address synthProxy, uint amountOfSynth) external requireRedemptionActive {
76+
// technically this check isn't necessary - Synth.burn would fail due to safe sub,
77+
// but this is a useful error message to the user
78+
require(IERC20(synthProxy).balanceOf(msg.sender) >= amountOfSynth, "Insufficient balance");
79+
_redeem(synthProxy, amountOfSynth);
80+
}
81+
82+
function _redeem(address synthProxy, uint amountOfSynth) internal {
83+
bytes32 currencyKey = ISynth(IProxy(synthProxy).target()).currencyKey();
84+
require(currencyKey != sUSD, "Cannot redeem sUSD");
85+
86+
// Discount rate applied to chainlink price for dynamic redemptions
87+
uint rateToRedeem = exchangeRates().rateForCurrency(currencyKey).multiplyDecimalRound(discountRate);
88+
require(rateToRedeem > 0, "Synth not redeemable");
89+
require(amountOfSynth > 0, "No balance of synth to redeem");
90+
91+
issuer().burnForRedemption(address(synthProxy), msg.sender, amountOfSynth);
92+
uint amountInsUSD = amountOfSynth.multiplyDecimalRound(rateToRedeem);
93+
issuer().issueSynthsWithoutDebt(sUSD, msg.sender, amountInsUSD);
94+
95+
emit SynthRedeemed(address(synthProxy), msg.sender, amountOfSynth, amountInsUSD);
96+
}
97+
98+
/* ========== MODIFIERS ========== */
99+
100+
modifier requireRedemptionActive() {
101+
redeemingActive();
102+
_;
103+
}
104+
105+
/* ========== RESTRICTED FUNCTIONS ========== */
106+
107+
function setDiscountRate(uint _newRate) external onlyOwner {
108+
require(_newRate >= 0 && _newRate <= SafeDecimalMath.unit(), "Invalid rate");
109+
discountRate = _newRate;
110+
emit DiscountRateUpdated(_newRate);
111+
}
112+
113+
function suspendRedemption() external onlyOwner {
114+
require(redemptionActive, "Redemption suspended");
115+
redemptionActive = false;
116+
emit RedemptionSuspended();
117+
}
118+
119+
function resumeRedemption() external onlyOwner {
120+
require(!redemptionActive, "Redemption not suspended");
121+
redemptionActive = true;
122+
emit RedemptionResumed();
123+
}
124+
125+
/* ========== EVENTS ========== */
126+
127+
event RedemptionSuspended();
128+
event RedemptionResumed();
129+
event DiscountRateUpdated(uint discountRate);
130+
event SynthRedeemed(address synth, address account, uint amountOfSynth, uint amountInsUSD);
131+
}

contracts/Issuer.sol

+14-19
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
8787
bytes32 private constant CONTRACT_LIQUIDATOR = "Liquidator";
8888
bytes32 private constant CONTRACT_LIQUIDATOR_REWARDS = "LiquidatorRewards";
8989
bytes32 private constant CONTRACT_DEBTCACHE = "DebtCache";
90+
bytes32 private constant CONTRACT_DYNAMICSYNTHREDEEMER = "DynamicSynthRedeemer";
9091
bytes32 private constant CONTRACT_SYNTHREDEEMER = "SynthRedeemer";
9192
bytes32 private constant CONTRACT_SYNTHETIXBRIDGETOOPTIMISM = "SynthetixBridgeToOptimism";
9293
bytes32 private constant CONTRACT_SYNTHETIXBRIDGETOBASE = "SynthetixBridgeToBase";
@@ -101,7 +102,7 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
101102
/* ========== VIEWS ========== */
102103
function resolverAddressesRequired() public view returns (bytes32[] memory addresses) {
103104
bytes32[] memory existingAddresses = MixinSystemSettings.resolverAddressesRequired();
104-
bytes32[] memory newAddresses = new bytes32[](14);
105+
bytes32[] memory newAddresses = new bytes32[](15);
105106
newAddresses[0] = CONTRACT_SYNTHETIX;
106107
newAddresses[1] = CONTRACT_EXCHANGER;
107108
newAddresses[2] = CONTRACT_EXRATES;
@@ -114,8 +115,9 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
114115
newAddresses[9] = CONTRACT_LIQUIDATOR_REWARDS;
115116
newAddresses[10] = CONTRACT_DEBTCACHE;
116117
newAddresses[11] = CONTRACT_SYNTHREDEEMER;
117-
newAddresses[12] = CONTRACT_EXT_AGGREGATOR_ISSUED_SYNTHS;
118-
newAddresses[13] = CONTRACT_EXT_AGGREGATOR_DEBT_RATIO;
118+
newAddresses[12] = CONTRACT_DYNAMICSYNTHREDEEMER;
119+
newAddresses[13] = CONTRACT_EXT_AGGREGATOR_ISSUED_SYNTHS;
120+
newAddresses[14] = CONTRACT_EXT_AGGREGATOR_DEBT_RATIO;
119121
return combineArrays(existingAddresses, newAddresses);
120122
}
121123

@@ -649,20 +651,6 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
649651
}
650652
}
651653

652-
/**
653-
* Function used to migrate balances from the CollateralShort contract
654-
* @param short The address of the CollateralShort contract to be upgraded
655-
* @param amount The amount of sUSD collateral to be burnt
656-
*/
657-
function upgradeCollateralShort(address short, uint amount) external onlyOwner {
658-
require(short == resolver.getAddress("CollateralShortLegacy"), "wrong address");
659-
require(amount > 0, "cannot burn 0 synths");
660-
661-
exchanger().settle(short, sUSD);
662-
663-
synths[sUSD].burn(short, amount);
664-
}
665-
666654
function issueSynths(address from, uint amount) external onlySynthetix {
667655
require(amount > 0, "cannot issue 0 synths");
668656

@@ -1034,7 +1022,11 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
10341022
address bridgeL1 = resolver.getAddress(CONTRACT_SYNTHETIXBRIDGETOOPTIMISM);
10351023
address bridgeL2 = resolver.getAddress(CONTRACT_SYNTHETIXBRIDGETOBASE);
10361024
address feePool = resolver.getAddress(CONTRACT_FEEPOOL);
1037-
require(msg.sender == bridgeL1 || msg.sender == bridgeL2 || msg.sender == feePool, "only trusted minters");
1025+
address dynamicSynthRedeemer = resolver.getAddress(CONTRACT_DYNAMICSYNTHREDEEMER);
1026+
require(
1027+
msg.sender == bridgeL1 || msg.sender == bridgeL2 || msg.sender == feePool || msg.sender == dynamicSynthRedeemer,
1028+
"only trusted minters"
1029+
);
10381030
_;
10391031
}
10401032

@@ -1047,7 +1039,10 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
10471039
}
10481040

10491041
function _onlySynthRedeemer() internal view {
1050-
require(msg.sender == address(synthRedeemer()), "Only SynthRedeemer");
1042+
require(
1043+
msg.sender == address(synthRedeemer()) || msg.sender == resolver.getAddress(CONTRACT_DYNAMICSYNTHREDEEMER),
1044+
"Only SynthRedeemer"
1045+
);
10511046
}
10521047

10531048
modifier onlySynthRedeemer() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
pragma solidity >=0.4.24;
2+
3+
import "./IERC20.sol";
4+
5+
interface IDynamicSynthRedeemer {
6+
function suspendRedemption() external;
7+
8+
function resumeRedemption() external;
9+
10+
// Rate applied to chainlink price for redemptions
11+
function getDiscountRate() external view returns (uint);
12+
13+
function redeem(address synthProxy) external;
14+
15+
function redeemAll(address[] calldata synthProxies) external;
16+
17+
function redeemPartial(address synthProxy, uint amountOfSynth) external;
18+
}

publish/deployed/local-ovm/config.json

+3
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@
236236
"SynthRedeemer": {
237237
"deploy": true
238238
},
239+
"DynamicSynthRedeemer": {
240+
"deploy": true
241+
},
239242
"OneNetAggregatorIssuedSynths": {
240243
"deploy": true
241244
},

publish/deployed/local/config.json

+3
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@
206206
"SynthRedeemer": {
207207
"deploy": true
208208
},
209+
"DynamicSynthRedeemer": {
210+
"deploy": true
211+
},
209212
"OneNetAggregatorIssuedSynths": {
210213
"deploy": true
211214
},

publish/releases.json

+13
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,11 @@
10681068
"layer": "ovm",
10691069
"sources": [],
10701070
"released": "ovm"
1071+
},
1072+
{
1073+
"sip": 2059,
1074+
"layer": "both",
1075+
"sources": ["DebtCache", "DynamicSynthRedeemer", "Issuer"]
10711076
}
10721077
],
10731078
"releases": [
@@ -2002,6 +2007,14 @@
20022007
},
20032008
"sips": [299],
20042009
"released": true
2010+
},
2011+
{
2012+
"name": "Acrab",
2013+
"version": {
2014+
"major": 2,
2015+
"minor": 101
2016+
},
2017+
"sips": [2059]
20052018
}
20062019
]
20072020
}

publish/src/commands/deploy/deploy-core.js

+6
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,12 @@ module.exports = async ({
330330
args: [addressOf(readProxyForResolver)],
331331
});
332332

333+
await deployer.deployContract({
334+
name: 'DynamicSynthRedeemer',
335+
deps: ['AddressResolver'],
336+
args: [account, addressOf(readProxyForResolver)],
337+
});
338+
333339
await deployer.deployContract({
334340
name: 'WrapperFactory',
335341
source: 'WrapperFactory',

0 commit comments

Comments
 (0)