From 967445f55a9937d9ee176d8b71ee5930f376f717 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Mon, 10 Feb 2025 22:54:55 +1100 Subject: [PATCH 1/2] add return USD function allows for conversion from v3 USD back to v2x USD --- markets/legacy-market/cannonfile.test.toml | 22 ++-- markets/legacy-market/cannonfile.toml | 2 +- .../legacy-market/contracts/LegacyMarket.sol | 29 +++++ .../contracts/interfaces/ILegacyMarket.sol | 17 +++ .../test/integration/LegacyMarket.ts | 122 ++++++++++++------ 5 files changed, 138 insertions(+), 54 deletions(-) diff --git a/markets/legacy-market/cannonfile.test.toml b/markets/legacy-market/cannonfile.test.toml index 499bd34d25..2ca7a3e348 100644 --- a/markets/legacy-market/cannonfile.test.toml +++ b/markets/legacy-market/cannonfile.test.toml @@ -29,7 +29,7 @@ from = "<%= settings.pool_owner %>" args = [ "<%= settings.sc_pool_id %>", [ - { marketId = "<%= extras.marketId %>", weightD18 = "1", maxDebtShareValueD18 = "1000000000000000000" }, + { marketId = "<%= settings.market_id %>", weightD18 = "1", maxDebtShareValueD18 = "1000000000000000000" }, ], ] depends = ["invoke.createPool"] @@ -41,15 +41,15 @@ args = ["<%= settings.sc_pool_id %>"] fromCall.func = "owner" depends = ["invoke.createPool"] -[invoke.registerRewardsDistributor] -target = ["v3.CoreProxy"] -func = "registerRewardsDistributor" -args = [ - "<%= settings.sc_pool_id %>", - "<%= imports.v2x.contracts.ProxySynthetix.address %>", - "<%= contracts.SNXDistributor.address %>", -] +#[invoke.registerRewardsDistributor] +#target = ["v3.CoreProxy"] +#func = "registerRewardsDistributor" +#args = [ +# "<%= settings.sc_pool_id %>", +# "<%= imports.v2x.contracts.ProxySynthetix.address %>", +# "<%= contracts.SNXDistributor.address %>", +#] -fromCall.func = "owner" +#fromCall.func = "owner" -depends = ["invoke.createPool", "contract.SNXDistributor"] +#depends = ["invoke.createPool"] diff --git a/markets/legacy-market/cannonfile.toml b/markets/legacy-market/cannonfile.toml index 5c0e5c6ced..09500296fc 100644 --- a/markets/legacy-market/cannonfile.toml +++ b/markets/legacy-market/cannonfile.toml @@ -12,7 +12,7 @@ defaultValue = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" defaultValue = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" [setting.v2x_package] -defaultValue = "synthetix:2.101.2" +defaultValue = "synthetix:2.101.3" [setting.v3_package] defaultValue = "synthetix:3.3.15" diff --git a/markets/legacy-market/contracts/LegacyMarket.sol b/markets/legacy-market/contracts/LegacyMarket.sol index 631d5ea8fb..3e1c8bbd91 100644 --- a/markets/legacy-market/contracts/LegacyMarket.sol +++ b/markets/legacy-market/contracts/LegacyMarket.sol @@ -135,6 +135,35 @@ contract LegacyMarket is ILegacyMarket, Ownable, UUPSImplementation, IMarket, IE return 0; } + /** + * @inheritdoc ILegacyMarket + */ + function returnUSD(uint256 amount) external override { + if (pauseStablecoinConversion) { + revert Paused(); + } + + if (amount == 0) { + revert ParameterError.InvalidParameter("amount", "Should be non-zero"); + } + + address sender = ERC2771Context._msgSender(); + + // get synthetix v2x addresses + IERC20 oldUSD = IERC20(v2xResolver.getAddress("ProxysUSD")); + ISynthetix oldSynthetix = ISynthetix(v2xResolver.getAddress("Synthetix")); + IIssuer iss = IIssuer(v2xResolver.getAddress("Issuer")); + + // retrieve the v3 sUSD from the user so we can burn it + v3System.depositMarketUsd(marketId, sender, amount); + + // now issue new synths and send them to the user + oldSynthetix.issueSynths(amount); + oldUSD.transfer(sender, amount); + + emit ReturnedUSD(ERC2771Context._msgSender(), amount); + } + /** * @inheritdoc ILegacyMarket */ diff --git a/markets/legacy-market/contracts/interfaces/ILegacyMarket.sol b/markets/legacy-market/contracts/interfaces/ILegacyMarket.sol index 783234bc11..6ab7b76ccd 100644 --- a/markets/legacy-market/contracts/interfaces/ILegacyMarket.sol +++ b/markets/legacy-market/contracts/interfaces/ILegacyMarket.sol @@ -51,6 +51,13 @@ interface ILegacyMarket { */ event ConvertedUSD(address indexed account, uint256 amount); + /** + * @notice Emitted after a call to `returnUSD`, moving debt from v3 to v2x. + * @param account the address of the address which provided the v3 sUSD for conversion + * @param amount the amount of v3 sUSD burnt, and the amount of v2x sUSD minted + */ + event ReturnedUSD(address indexed account, uint256 amount); + /** * @notice Emitted after a call to `setPauseStablecoinConversion` * @param sender the address setting the stablecoin conversion pause status @@ -65,6 +72,16 @@ interface ILegacyMarket { */ event PauseMigrationSet(address indexed sender, bool paused); + /** + * @notice Called by anyone with {amount} v3 sUSD to convert {amount} v3 sUSD to {amount} v2x sUSD. + * The v3 sUSD will be burned (thereby reducing the sUSD total supply and v3 system size), and v2x sUSD will be minted. + * Any user who has v3 sUSD can call this function. + * Requirements: + * * User must first approve() the legacy market contract to spend the user's v3 sUSD + * @param amount the quantity to convert + */ + function returnUSD(uint256 amount) external; + /** * @notice Called by anyone with {amount} sUSD to convert {amount} sUSD to {amount} snxUSD. * The sUSD will be burned (thereby reducing the sUSD total supply and v2x system size), and snxUSD will be minted. diff --git a/markets/legacy-market/test/integration/LegacyMarket.ts b/markets/legacy-market/test/integration/LegacyMarket.ts index 9c0e306aa4..92eccfcf56 100644 --- a/markets/legacy-market/test/integration/LegacyMarket.ts +++ b/markets/legacy-market/test/integration/LegacyMarket.ts @@ -43,7 +43,6 @@ describe('LegacyMarket', function () { let synthetixDebtShare: ethers.Contract; let liquidationRewards: ethers.Contract; let rewardEscrow: ethers.Contract; - let snxDistributor: ethers.Contract; let v3System: ethers.Contract; let v3Account: ethers.Contract; @@ -118,11 +117,6 @@ describe('LegacyMarket', function () { provider ); - snxDistributor = new ethers.Contract( - outputs.contracts.SNXDistributor.address, - outputs.contracts.SNXDistributor.abi - ); - await rewardEscrow .connect(await getImpersonatedSigner(provider, await rewardEscrow.owner())) .setPermittedEscrowCreator(await owner.getAddress(), true); @@ -218,6 +212,86 @@ describe('LegacyMarket', function () { }); }); + describe('returnUSD()', async () => { + before(restore); + + before('approve', async () => { + await susdToken.connect(snxStaker).approve(market.address, ethers.constants.MaxUint256); + }); + + describe('when some snxUSD is available and collateral has been migrated', async () => { + const convertedAmount = wei(1); + + before('do migration', async () => { + await snxToken.connect(snxStaker).approve(market.address, ethers.constants.MaxUint256); + await market.connect(snxStaker).migrate(migratedAccountId); + + // sanity + assertBn.gte( + await v3System.getWithdrawableMarketUsd(await market.marketId()), + convertedAmount.toBN() + ); + }); + + before('do convert', async () => { + await (await market.connect(snxStaker).convertUSD(convertedAmount.toBN())).wait(); + }); + + it('fails when returnUSD argument is 0', async () => { + await assertRevert(market.connect(snxStaker).returnUSD(0), 'InvalidParameter(\\"amount\\"'); + }); + + it('fails when insufficient source balance', async () => { + // transfer away some of the sUSD so that we can see what happens when there is not balance + await susdToken.connect(snxStaker).transfer(await owner.getAddress(), wei(500).toBN()); + await assertRevert( + market.connect(snxStaker).convertUSD(wei(501).toBN()), + 'Error(\\"Insufficient balance after any settlement owing\\")' + ); + }); + + describe('when invoked', async () => { + let txn: ethers.providers.TransactionReceipt; + + let beforeMarketBalance: Wei; + + before('record priors', async () => { + beforeMarketBalance = wei(await v3System.getMarketNetIssuance(await market.marketId())); + }); + + before('when invoked', async () => { + await ( + await v3Usd.connect(snxStaker).approve(market.address, convertedAmount.toBN()) + ).wait(); + txn = await (await market.connect(snxStaker).returnUSD(convertedAmount.toBN())).wait(); + }); + + it('mints v2x USD', async () => { + assertBn.equal(await susdToken.balanceOf(await snxStaker.getAddress()), wei(500).toBN()); + }); + + it('burns v3 USD', async () => { + assertBn.equal(await v3Usd.balanceOf(await snxStaker.getAddress()), 0); + }); + + it('increased market balance', async () => { + assertBn.equal( + await v3System.getMarketNetIssuance(await market.marketId()), + beforeMarketBalance.sub(1).toBN() + ); + }); + + it('emitted an event', async () => { + await assertEvent( + txn, + `ReturnedUSD("${snxStakerAddress}", ${convertedAmount.toBN().toString()})`, + market + ); + }); + }); + }); + }); + function testMigrate(doMigrateCall: () => Promise) { describe('with a fake account with escrow entries and liquidation rewards', async () => { before('create escrow entries', async () => { @@ -438,42 +512,6 @@ describe('LegacyMarket', function () { ethers.utils.parseEther('1000') ); }); - - it('rewards available for distributed', async () => { - assertBn.equal( - await v3System - .connect(owner) - .callStatic.claimRewards( - accountId, - await v3System.getPreferredPool(), - snxToken.address, - snxDistributor.address - ), - ethers.utils.parseEther('10') - ); - }); - - describe('claim rewards from rewards distributor', async () => { - let beforeBalance: ethers.BigNumberish; - before('claim', async () => { - beforeBalance = await snxToken.balanceOf(await owner.getAddress()); - await v3System - .connect(owner) - .claimRewards( - accountId, - await v3System.getPreferredPool(), - snxToken.address, - snxDistributor.address - ); - }); - - it('should have collateral in account', async () => { - assertBn.equal( - (await snxToken.balanceOf(await owner.getAddress())).sub(beforeBalance), - ethers.utils.parseEther('10') - ); - }); - }); }); }); }); From f472d4abe19cb2aa10a28514e1e23c6ac33c37f3 Mon Sep 17 00:00:00 2001 From: Daniel Beal Date: Tue, 18 Feb 2025 09:47:49 +1100 Subject: [PATCH 2/2] fix test build for treasury market --- markets/treasury-market/cannonfile.test.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/markets/treasury-market/cannonfile.test.toml b/markets/treasury-market/cannonfile.test.toml index f334c258d4..1fa935707d 100644 --- a/markets/treasury-market/cannonfile.test.toml +++ b/markets/treasury-market/cannonfile.test.toml @@ -15,7 +15,7 @@ defaultValue = "1" defaultValue = "<%= imports.v3.contracts.CollateralMock.address %>" [setting.v3_package] -defaultValue = "synthetix:3.10.3-testable" +defaultValue = "synthetix:3.10.4-testable" depends = []