Skip to content

Commit 37b4de7

Browse files
authored
Acrab release v2.101.1-alpha (#2229)
* SIP-2059: Legacy Spot Synth Migration (#2227) * sepolia-ethereum deployment artifacts * update verify * sepolia-ovm deployment artifacts * fix: check valid synth (#2231) * add check for valid synth target * remove unnecessary address casts * impl feedback and update tests * replace proxy param with currency key * add burnAndIssueSynthsWithoutDebtCache func
1 parent 648fd5e commit 37b4de7

24 files changed

+2114
-185
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

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

contracts/Issuer.sol

+35-18
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

@@ -633,6 +635,32 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
633635
return rateInvalid;
634636
}
635637

638+
/**
639+
* SIP-2059: Dynamic Redemption
640+
* Function used to burn spot synths and issue the equivalent in sUSD at chainlink price * discount rate
641+
* @param account The address of the account that is redeeming
642+
* @param currencyKey The synth to be redeemed
643+
* @param amountOfSynth The amount of redeeming synth to burn
644+
* @param amountInsUSD The amount of sUSD to issue
645+
*/
646+
function burnAndIssueSynthsWithoutDebtCache(
647+
address account,
648+
bytes32 currencyKey,
649+
uint amountOfSynth,
650+
uint amountInsUSD
651+
) external onlySynthRedeemer {
652+
exchanger().settle(account, currencyKey);
653+
654+
// Burn their redeemed synths
655+
synths[currencyKey].burn(account, amountOfSynth);
656+
657+
// record issue timestamp
658+
_setLastIssueEvent(account);
659+
660+
// Issuer their sUSD equivalent
661+
synths[sUSD].issue(account, amountInsUSD);
662+
}
663+
636664
/**
637665
* SIP-237: Debt Migration
638666
* Function used for the one-way migration of all debt and liquid + escrowed SNX from L1 -> L2
@@ -649,20 +677,6 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
649677
}
650678
}
651679

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-
666680
function issueSynths(address from, uint amount) external onlySynthetix {
667681
require(amount > 0, "cannot issue 0 synths");
668682

@@ -1047,7 +1061,10 @@ contract Issuer is Owned, MixinSystemSettings, IIssuer {
10471061
}
10481062

10491063
function _onlySynthRedeemer() internal view {
1050-
require(msg.sender == address(synthRedeemer()), "Only SynthRedeemer");
1064+
require(
1065+
msg.sender == address(synthRedeemer()) || msg.sender == resolver.getAddress(CONTRACT_DYNAMICSYNTHREDEEMER),
1066+
"Only SynthRedeemer"
1067+
);
10511068
}
10521069

10531070
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(bytes32 currencyKey) external;
14+
15+
function redeemAll(bytes32[] calldata currencyKeys) external;
16+
17+
function redeemPartial(bytes32 currencyKey, uint amountOfSynth) external;
18+
}

contracts/interfaces/IIssuer.sol

+7
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,12 @@ interface IIssuer {
131131
uint amount
132132
) external returns (bool rateInvalid);
133133

134+
function burnAndIssueSynthsWithoutDebtCache(
135+
address account,
136+
bytes32 currencyKey,
137+
uint amountOfSynth,
138+
uint amountInsUSD
139+
) external;
140+
134141
function modifyDebtSharesForMigration(address account, uint amount) external;
135142
}

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/deployed/sepolia-ovm/config.json

+3
Original file line numberDiff line numberDiff line change
@@ -2056,5 +2056,8 @@
20562056
},
20572057
"PerpsV2MarketViewsPENDLEPERP": {
20582058
"deploy": false
2059+
},
2060+
"DynamicSynthRedeemer": {
2061+
"deploy": false
20592062
}
20602063
}

publish/deployed/sepolia-ovm/deployment.json

+480-42
Large diffs are not rendered by default.

publish/deployed/sepolia/config.json

+3
Original file line numberDiff line numberDiff line change
@@ -205,5 +205,8 @@
205205
},
206206
"DebtMigratorOnEthereum": {
207207
"deploy": false
208+
},
209+
"DynamicSynthRedeemer": {
210+
"deploy": false
208211
}
209212
}

0 commit comments

Comments
 (0)