diff --git a/common/constants.ts b/common/constants.ts index 9ff641460..44e7e9f75 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -64,6 +64,7 @@ export enum BidType { NONE, CALLBACK, TRANSFER, + FILL, } export const FURNACE_DEST = '0x0000000000000000000000000000000000000001' diff --git a/contracts/p1/mixins/GlobalReentrancyGuard.sol b/contracts/p1/mixins/GlobalReentrancyGuard.sol index 9333cb040..30ada9c36 100644 --- a/contracts/p1/mixins/GlobalReentrancyGuard.sol +++ b/contracts/p1/mixins/GlobalReentrancyGuard.sol @@ -77,13 +77,4 @@ abstract contract GlobalReentrancyGuard is Initializable { // https://eips.ethereum.org/EIPS/eip-2200) $._status = NOT_ENTERED; } - - /** - * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates - * there is a `nonReentrant` function in the call stack. - */ - function _reentrancyGuardEntered() internal view returns (bool) { - ReentrancyGuardStorage storage $ = _getReentrancyGuardStorage(); - return $._status == ENTERED; - } } diff --git a/contracts/plugins/mocks/CowSwapFillerMock.sol b/contracts/plugins/mocks/CowSwapFillerMock.sol index 27cc9e995..d3485a819 100644 --- a/contracts/plugins/mocks/CowSwapFillerMock.sol +++ b/contracts/plugins/mocks/CowSwapFillerMock.sol @@ -45,6 +45,9 @@ contract CowSwapFillerMock is Initializable, IBaseTrustedFiller { uint256 public price; // D27{buyTok/sellTok} bool public partiallyFillable; + // mock: allow to force swapActive on tests + bool public forceSwapActive; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -93,6 +96,11 @@ contract CowSwapFillerMock is Initializable, IBaseTrustedFiller { /// @return true if the contract is mid-swap and funds have not yet settled function swapActive() public view returns (bool) { + // mock: used for testing + if (forceSwapActive) { + return true; + } + if (block.number != blockInitialized) { return false; } @@ -135,4 +143,9 @@ contract CowSwapFillerMock is Initializable, IBaseTrustedFiller { token.safeTransfer(fillCreator, tokenBalance); } } + + /// Mock: Setter for forceSwapActive + function setForceSwapActive(bool _forceSwapActive) external { + forceSwapActive = _forceSwapActive; + } } diff --git a/contracts/plugins/trading/DutchTrade.sol b/contracts/plugins/trading/DutchTrade.sol index 58187b775..a677b2c34 100644 --- a/contracts/plugins/trading/DutchTrade.sol +++ b/contracts/plugins/trading/DutchTrade.sol @@ -23,7 +23,8 @@ interface IDutchTradeCallee { enum BidType { NONE, CALLBACK, - TRANSFER + TRANSFER, + FILL } // A dutch auction in 4 parts: @@ -123,6 +124,7 @@ contract DutchTrade is ITrade, Versioned { // === Trusted Fillers === IBaseTrustedFiller public activeTrustedFill; + uint192 public savedFillPrice; // cached trusted fill price // This modifier both enforces the state-machine pattern and guards against reentrancy. modifier stateTransition(TradeStatus begin, TradeStatus end) { @@ -303,6 +305,7 @@ contract DutchTrade is ITrade, Versioned { closeTrustedFiller returns (IBaseTrustedFiller filler) { + require(bidder == address(0), "bid already received"); require(status == TradeStatus.OPEN, "trade not open"); // Get trusted filler registry @@ -311,9 +314,12 @@ contract DutchTrade is ITrade, Versioned { require(address(registry) != address(0) && enabled, "trusted fillers not enabled"); // Get current price and amounts - uint192 price = _price(uint48(block.timestamp)); + savedFillPrice = _price(uint48(block.timestamp)); uint256 sellAmt = lot(); // {qSellTok} - uint256 buyAmt = _bidAmount(price); // {qBuyTok} + uint256 buyAmt = _bidAmount(savedFillPrice); // {qBuyTok} + + // Mark bid type + bidType = BidType.FILL; // Create trusted filler filler = registry.createTrustedFiller(msg.sender, targetFiller, deploymentSalt); @@ -341,23 +347,26 @@ contract DutchTrade is ITrade, Versioned { { require(msg.sender == address(origin), "only origin can settle"); - // If auction has ended -> continue - if (block.timestamp <= endTime) { - bool filled = false; - if (block.timestamp >= startTime) { - // Ongoing auction, check if can be settled - uint192 price = _price(uint48(block.timestamp)); - uint256 amountIn = _bidAmount(price); - filled = buy.balanceOf(address(this)) >= amountIn; + bool filled = false; + if (bidType == BidType.FILL) { + uint256 amountIn = _bidAmount(savedFillPrice); + filled = buy.balanceOf(address(this)) >= amountIn; + + // reportViolation if filled in geometric phase + if (filled && savedFillPrice > bestPrice.mul(ONE_POINT_FIVE, CEIL)) { + broker.reportViolation(); } - require(bidder != address(0) || filled, "auction not over"); } + require(bidder != address(0) || filled || block.timestamp > endTime, "auction not over"); + if (bidType == BidType.CALLBACK) { soldAmt = lot(); // {qSellTok} } else if (bidType == BidType.TRANSFER) { soldAmt = lot(); // {qSellTok} sell.safeTransfer(bidder, soldAmt); // {qSellTok} + } else if (bidType == BidType.FILL && filled) { + soldAmt = lot(); // {qSellTok} } // Transfer remaining balances back to origin @@ -385,17 +394,23 @@ contract DutchTrade is ITrade, Versioned { if (block.timestamp > endTime) { return true; } + // OPEN with active fill -> false + if (address(activeTrustedFill) != address(0) && activeTrustedFill.swapActive()) { + return false; + } - // Ongoing OPEN auction, check if can be settled - uint192 price = _price(uint48(block.timestamp)); - uint256 amountIn = _bidAmount(price); - - uint256 amountInFiller = address(activeTrustedFill) != address(0) - ? buy.balanceOf(address(activeTrustedFill)) - : 0; - uint256 amountInTrade = buy.balanceOf(address(this)); + // Ongoing OPEN auction, no active fill, check if can be settled + bool filled = false; + if (bidType == BidType.FILL) { + uint256 amountIn = _bidAmount(savedFillPrice); + uint256 amountInFiller = address(activeTrustedFill) != address(0) + ? buy.balanceOf(address(activeTrustedFill)) + : 0; + uint256 amountInTrade = buy.balanceOf(address(this)); + filled = amountInFiller + amountInTrade >= amountIn; + } - return (bidder != address(0) || amountInFiller + amountInTrade >= amountIn); + return (bidder != address(0) || filled); } // === Private === diff --git a/package.json b/package.json index b5de2320d..2922da8eb 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "@aave/core-v3": "^1.18.0", "@aave/periphery-v3": "^2.5.0", "@nomicfoundation/hardhat-toolbox": "^2.0.1", - "@reserve-protocol/trusted-fillers": "github:reserve-protocol/trusted-fillers#6ac118cc4a353e17712964d194814efce878363d", + "@reserve-protocol/trusted-fillers": "github:reserve-protocol/trusted-fillers#a3fdf80204aa2915be313641590ff5c3be9a6c8e", "@types/isomorphic-fetch": "^0.0.36", "axios-retry": "^4.1.0", "cheerio": "^1.0.0-rc.12", diff --git a/test/Broker.test.ts b/test/Broker.test.ts index 904fd3787..4834a358b 100644 --- a/test/Broker.test.ts +++ b/test/Broker.test.ts @@ -1574,9 +1574,12 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { const getNextTradeAddress = async (tradeRequest: ITradeRequest): Promise => { let tradeAddress = '' + const sellColl = await ethers.getContractAt('ICollateral', tradeRequest.sell) + const sellToken = await ethers.getContractAt('ERC20Mock', await sellColl.erc20()) + await whileImpersonating(backingManager.address, async (bmSigner) => { // set approval for broker - await token0.connect(bmSigner).approve(broker.address, tradeRequest.sellAmount) + await sellToken.connect(bmSigner).approve(broker.address, tradeRequest.sellAmount) tradeAddress = await broker.connect(bmSigner).callStatic.openTrade( TradeKind.BATCH_AUCTION, // workaround to by-pass price checks tradeRequest, @@ -1626,6 +1629,9 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { expect(await dutchTrade.status()).to.equal(TradeStatus.OPEN) expect(await dutchTrade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await dutchTrade.bidType()).to.equal(BidType.NONE) + expect(await dutchTrade.savedFillPrice()).to.equal(0) + // Create trusted fill await expect( dutchTrade @@ -1637,6 +1643,11 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { const activeFill = await dutchTrade.activeTrustedFill() expect(activeFill).to.not.equal(ZERO_ADDRESS) + // Verify price was cached and type set + expect(await dutchTrade.bidType()).to.equal(BidType.FILL) + const prevSavedFillPrice = await dutchTrade.savedFillPrice() + expect(prevSavedFillPrice).to.be.gt(0) + // Try to create second fill - will close previous one const prevFill = activeFill await expect( @@ -1648,6 +1659,9 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { // Check new trusted fill status expect(await dutchTrade.activeTrustedFill()).to.not.equal(ZERO_ADDRESS) expect(await dutchTrade.activeTrustedFill()).to.not.equal(prevFill) + + // New price was cached + expect(await dutchTrade.savedFillPrice()).to.be.lt(prevSavedFillPrice) }) it('Should not create trusted fill when registry not enabled', async () => { @@ -1721,6 +1735,9 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { expect(await dutchTrade.status()).to.equal(TradeStatus.OPEN) expect(await dutchTrade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await dutchTrade.bidType()).to.equal(BidType.NONE) + expect(await dutchTrade.savedFillPrice()).to.equal(0) + // Check balances expect(await token0.balanceOf(backingManager.address)).to.equal( initBal0.sub(tradeRequest.sellAmount) @@ -1732,6 +1749,14 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { .connect(addr1) .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) + // Use cached price at creation + const bidAmount = await dutchTrade.bidAmount(await getLatestBlockTimestamp()) + + // Verify price was cached and type set + expect(await dutchTrade.bidType()).to.equal(BidType.FILL) + const prevSavedFillPrice = await dutchTrade.savedFillPrice() + expect(prevSavedFillPrice).to.be.gt(0) + const activeFill = await dutchTrade.activeTrustedFill() expect(activeFill).to.not.equal(ZERO_ADDRESS) @@ -1740,7 +1765,6 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { // Perform fill await token0.burn(activeFill, tradeRequest.sellAmount) - const bidAmount = await dutchTrade.bidAmount(await getLatestBlockTimestamp()) await token1.mint(activeFill, bidAmount) // The trade should be settleable with an active trusted fill @@ -1751,6 +1775,9 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { await expect(dutchTrade.connect(bmSigner).settle()).to.not.be.reverted }) + // Used cached price + expect(await dutchTrade.savedFillPrice()).to.equal(prevSavedFillPrice) + // After settling, the trusted fill should be closed expect(await dutchTrade.activeTrustedFill()).to.equal(ZERO_ADDRESS) expect(await dutchTrade.status()).to.equal(TradeStatus.CLOSED) @@ -1816,6 +1843,7 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { expect(await token1.balanceOf(activeFill)).to.be.lt(tradeRequest.minBuyAmount) // The trade should not be settleable + expect(await dutchTrade.bidType()).to.equal(BidType.FILL) expect(await dutchTrade.canSettle()).to.equal(false) // Cannot settle the trade @@ -1843,6 +1871,81 @@ describe(`BrokerP${IMPLEMENTATION} contract #fast`, () => { initBal1.add(tradeRequest.minBuyAmount.div(2)) ) }) + + it('Should not allow to settle trade if swap active', async () => { + // Create and setup a Dutch trade + const tradeAddress = await getNextTradeAddress(tradeRequest) + + // Get current balances + const initBal0 = await token0.balanceOf(backingManager.address) + const initBal1 = await token1.balanceOf(backingManager.address) + + // Set automine to false + await hre.network.provider.send('evm_setAutomine', [false]) + + // Create Dutch trade (need to refresh collaterals in the same timestamp) + await assetRegistry.refresh() + await whileImpersonating(backingManager.address, async (bmSigner) => { + await token0.connect(bmSigner).approve(broker.address, tradeRequest.sellAmount) + await broker.connect(bmSigner).openTrade(TradeKind.DUTCH_AUCTION, tradeRequest, prices) + }) + + // Mine block and reset automine + await hre.network.provider.send('evm_mine', []) + await hre.network.provider.send('evm_setAutomine', [true]) + + // Check trade status + dutchTrade = await ethers.getContractAt('DutchTrade', tradeAddress) + expect(await dutchTrade.status()).to.equal(TradeStatus.OPEN) + expect(await dutchTrade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + + // Check balances + expect(await token0.balanceOf(backingManager.address)).to.equal( + initBal0.sub(tradeRequest.sellAmount) + ) + expect(await token1.balanceOf(backingManager.address)).to.equal(initBal1) + + // Create trusted fill + await dutchTrade + .connect(addr1) + .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) + + // Use cached price at creation + const bidAmount = await dutchTrade.bidAmount(await getLatestBlockTimestamp()) + + // Set swap active + const activeFill = await dutchTrade.activeTrustedFill() + const cowswapFillerMock = await ethers.getContractAt('CowSwapFillerMock', activeFill) + await cowswapFillerMock.setForceSwapActive(true) + + // Perform fill + await token0.burn(activeFill, tradeRequest.sellAmount) + await token1.mint(activeFill, bidAmount) + + // Cannot settle at this point + await expect(await dutchTrade.canSettle()).to.equal(false) + + // End swap active + await cowswapFillerMock.setForceSwapActive(false) + + // The trade should now be settleable + expect(await dutchTrade.canSettle()).to.equal(true) + + // Settle the trade + await whileImpersonating(backingManager.address, async (bmSigner) => { + await expect(dutchTrade.connect(bmSigner).settle()).to.not.be.reverted + }) + + // After settling, the trusted fill should be closed + expect(await dutchTrade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await dutchTrade.status()).to.equal(TradeStatus.CLOSED) + + // Check balances + expect(await token0.balanceOf(backingManager.address)).to.equal( + initBal0.sub(tradeRequest.sellAmount) + ) + expect(await token1.balanceOf(backingManager.address)).to.equal(initBal1.add(bidAmount)) + }) }) }) diff --git a/test/Recollateralization.test.ts b/test/Recollateralization.test.ts index d19467fbb..dc912962b 100644 --- a/test/Recollateralization.test.ts +++ b/test/Recollateralization.test.ts @@ -9,6 +9,7 @@ import { BN_SCALE_FACTOR, CollateralStatus, TradeKind, + TradeStatus, MAX_UINT256, ZERO_ADDRESS, } from '../common/constants' @@ -17,6 +18,7 @@ import { bn, fp, pow10, toBNDecimals, divCeil } from '../common/numbers' import { Asset, ATokenFiatCollateral, + CowSwapFillerMock, CTokenMock, DutchTrade, ERC20Mock, @@ -24,6 +26,7 @@ import { FiatCollateral, GnosisMock, IAssetRegistry, + MockRoleRegistry, MockV3Aggregator, RTokenAsset, StaticATokenMock, @@ -34,6 +37,7 @@ import { TestIMain, TestIRToken, TestIStRSR, + TrustedFillerRegistry, USDCMock, DutchTradeRouter, } from '../typechain' @@ -58,6 +62,8 @@ import { mintCollaterals } from './utils/tokens' const DEFAULT_THRESHOLD = fp('0.01') // 1% +const describeP1 = IMPLEMENTATION == Implementation.P1 ? describe : describe.skip + const describeGas = IMPLEMENTATION == Implementation.P1 && useEnv('REPORT_GAS') ? describe.only : describe.skip @@ -3479,6 +3485,148 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => { ) }) }) + + describeP1('Trusted Fillers', () => { + let trustedFillerRegistry: TrustedFillerRegistry + let cowSwapFillerMock: CowSwapFillerMock + + beforeEach(async () => { + // Setup trusted filler registry and enable on Broker + const MockRoleRegistryFactory = await ethers.getContractFactory('MockRoleRegistry') + const mockRoleRegistry = await MockRoleRegistryFactory.deploy() + + const TrustedFillerRegistryFactory = await ethers.getContractFactory( + 'TrustedFillerRegistry' + ) + trustedFillerRegistry = ( + await TrustedFillerRegistryFactory.deploy(mockRoleRegistry.address) + ) + + const CowSwapFillerMockFactory = await ethers.getContractFactory('CowSwapFillerMock') + cowSwapFillerMock = await CowSwapFillerMockFactory.deploy() + + await trustedFillerRegistry.addTrustedFiller(cowSwapFillerMock.address) + + await broker + .connect(owner) + .setTrustedFillerRegistry(trustedFillerRegistry.address, true) + }) + + it('Should disable dutch trades with reportViolation', async () => { + await backingManager.rebalance(TradeKind.DUTCH_AUCTION) + const trade = await ethers.getContractAt( + 'DutchTrade', + await backingManager.trades(token0.address) + ) + + expect(await trade.status()).to.equal(TradeStatus.OPEN) + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await backingManager.tradesOpen()).to.equal(1) + + // Check broker not disabled + expect(await broker.dutchTradeDisabled(token0.address)).to.equal(false) + + // check balances + expect(await token0.balanceOf(trade.address)).to.equal(issueAmount) + expect(await token1.balanceOf(trade.address)).to.equal(0) + + // Create trusted fill on geometric phase + await expect( + trade + .connect(addr1) + .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) + ).to.emit(trade, 'TrustedFillCreated') + + // Use cached price at creation + const bidAmount = await trade.bidAmount(await getLatestBlockTimestamp()) + + // Funds moved to filler + expect(await token0.balanceOf(trade.address)).to.equal(0) + + // Verify active trusted fill is set + const activeFill = await trade.activeTrustedFill() + expect(activeFill).to.not.equal(ZERO_ADDRESS) + + // Perform fill during geometric phase + await token0.burn(activeFill, await token0.balanceOf(activeFill)) + await token1.mint(activeFill, bidAmount) + + // The trade should be settleable + expect(await trade.canSettle()).to.equal(true) + + // No sell tokens left on trade + expect(await token0.balanceOf(trade.address)).to.equal(0) + expect(await trade.status()).to.equal(TradeStatus.OPEN) + + // Settle trade + await expect(backingManager.settleTrade(token0.address)).to.emit( + backingManager, + 'TradeSettled' + ) + + expect(await trade.status()).to.equal(TradeStatus.CLOSED) + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await backingManager.tradesOpen()).to.equal(0) + + // Broker disabled, violation reported + expect(await broker.dutchTradeDisabled(token0.address)).to.equal(true) + expect(await broker.dutchTradeDisabled(token1.address)).to.equal(true) + + // All funds moved to origin + expect(await token0.balanceOf(backingManager.address)).to.equal(0) + expect(await rsr.balanceOf(backingManager.address)).to.be.closeTo(0, 5000) + expect(await token0.balanceOf(trade.address)).to.equal(0) + expect(await rsr.balanceOf(trade.address)).to.equal(0) + expect(await token0.balanceOf(activeFill)).to.equal(0) + expect(await rsr.balanceOf(activeFill)).to.equal(0) + }) + + it('Should not reportViolation if not filled', async () => { + await backingManager.rebalance(TradeKind.DUTCH_AUCTION) + const trade = await ethers.getContractAt( + 'DutchTrade', + await backingManager.trades(token0.address) + ) + + expect(await trade.status()).to.equal(TradeStatus.OPEN) + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await backingManager.tradesOpen()).to.equal(1) + + // Check broker not disabled + expect(await broker.dutchTradeDisabled(token0.address)).to.equal(false) + + // Create trusted fill on geometric phase + await expect( + trade + .connect(addr1) + .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) + ).to.emit(trade, 'TrustedFillCreated') + + // Use cached price at creation + const bidAmount = await trade.bidAmount(await getLatestBlockTimestamp()) + + // Verify active trusted fill is set + const activeFill = await trade.activeTrustedFill() + expect(activeFill).to.not.equal(ZERO_ADDRESS) + + // Advance time until auction ended, no fill nor bid + await advanceTime(config.dutchAuctionLength.add(100).toString()) + + // Settle trade + await expect(backingManager.settleTrade(token0.address)).to.emit( + backingManager, + 'TradeSettled' + ) + + expect(await trade.status()).to.equal(TradeStatus.CLOSED) + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await backingManager.tradesOpen()).to.equal(0) + + // Broker not disabled + expect(await broker.dutchTradeDisabled(token0.address)).to.equal(false) + expect(await broker.dutchTradeDisabled(token1.address)).to.equal(false) + }) + }) }) }) diff --git a/test/Revenues.test.ts b/test/Revenues.test.ts index cb78a5ec5..5289c4da8 100644 --- a/test/Revenues.test.ts +++ b/test/Revenues.test.ts @@ -3923,12 +3923,19 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rsr.balanceOf(trade.address)).to.equal(0) // Create trusted fill + expect(await trade.bidType()).to.equal(BidType.NONE) + await expect( trade .connect(addr1) .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) ).to.emit(trade, 'TrustedFillCreated') + expect(await trade.bidType()).to.equal(BidType.FILL) + + // Use cached price at creation + const bidAmount = await trade.bidAmount(await getLatestBlockTimestamp()) + // Funds moved to filler expect(await token0.balanceOf(trade.address)).to.equal(0) @@ -3941,7 +3948,6 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { // Perform fill await token0.burn(activeFill, await token0.balanceOf(activeFill)) - const bidAmount = await trade.bidAmount(await getLatestBlockTimestamp()) await rsr.mint(activeFill, bidAmount) // The trade should be settleable @@ -3974,6 +3980,76 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await rsr.balanceOf(activeFill)).to.equal(0) }) + it('Should not disable revenue dutch trades with reportViolation', async () => { + expect(await rsr.balanceOf(rsrTrader.address)).to.equal(0) + + await token0.connect(addr1).transfer(rsrTrader.address, issueAmount.div(2000)) + await rsrTrader.manageTokens([token0.address], [TradeKind.DUTCH_AUCTION]) + + const trade = await ethers.getContractAt( + 'DutchTrade', + await rsrTrader.trades(token0.address) + ) + + expect(await trade.status()).to.equal(TradeStatus.OPEN) + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await rsrTrader.tradesOpen()).to.equal(1) + + // Check broker not disabled + expect(await broker.dutchTradeDisabled(token0.address)).to.equal(false) + + // check balances + expect(await token0.balanceOf(trade.address)).to.equal(issueAmount.div(2000)) + expect(await rsr.balanceOf(trade.address)).to.equal(0) + + // Create trusted fill + await expect( + trade + .connect(addr1) + .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) + ).to.emit(trade, 'TrustedFillCreated') + + // Use cached price at creation + const bidAmount = await trade.bidAmount(await getLatestBlockTimestamp()) + + // Funds moved to filler + expect(await token0.balanceOf(trade.address)).to.equal(0) + + // Verify active trusted fill is set + const activeFill = await trade.activeTrustedFill() + expect(activeFill).to.not.equal(ZERO_ADDRESS) + + // Perform fill during geometric phase + await token0.burn(activeFill, await token0.balanceOf(activeFill)) + await rsr.mint(activeFill, bidAmount) + + // The trade should be settleable + expect(await trade.canSettle()).to.equal(true) + + // No sell tokens left on trade + expect(await token0.balanceOf(trade.address)).to.equal(0) + expect(await trade.status()).to.equal(TradeStatus.OPEN) + + // Settle trade + await rsrTrader.settleTrade(token0.address) + + expect(await trade.status()).to.equal(TradeStatus.CLOSED) + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await rsrTrader.tradesOpen()).to.equal(0) + + // Broker is not disabled, even though violation was reported + expect(await broker.dutchTradeDisabled(token0.address)).to.equal(false) + expect(await broker.dutchTradeDisabled(rsr.address)).to.equal(false) + + // All funds moved to origin + expect(await token0.balanceOf(rsrTrader.address)).to.equal(0) + expect(await rsr.balanceOf(rsrTrader.address)).to.be.closeTo(0, 5000) + expect(await token0.balanceOf(trade.address)).to.equal(0) + expect(await rsr.balanceOf(trade.address)).to.equal(0) + expect(await token0.balanceOf(activeFill)).to.equal(0) + expect(await rsr.balanceOf(activeFill)).to.equal(0) + }) + const bidTypes = [BidType.CALLBACK, BidType.TRANSFER] bidTypes.forEach((bidType) => { it(`Should allow to bid closing trusted filler - Bid Type: ${ @@ -4006,6 +4082,8 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) ).to.emit(trade, 'TrustedFillCreated') + expect(await trade.bidType()).to.equal(BidType.FILL) + // Funds moved to filler expect(await token0.balanceOf(trade.address)).to.equal(0) @@ -4070,6 +4148,92 @@ describe(`Revenues - P${IMPLEMENTATION}`, () => { expect(await token0.balanceOf(activeFill)).to.equal(0) expect(await rsr.balanceOf(activeFill)).to.equal(0) }) + + it(`Should not allow trusted fillers if bid already received - Bid Type: ${ + Object.values(BidType)[bidType] + }`, async () => { + const router = await (await ethers.getContractFactory('DutchTradeRouter')).deploy() + + expect(await rsr.balanceOf(rsrTrader.address)).to.equal(0) + + await token0.connect(addr1).transfer(rsrTrader.address, issueAmount.div(2000)) + await rsrTrader.manageTokens([token0.address], [TradeKind.DUTCH_AUCTION]) + + const trade = await ethers.getContractAt( + 'DutchTrade', + await rsrTrader.trades(token0.address) + ) + + expect(await trade.bidType()).to.equal(BidType.NONE) + expect(await trade.status()).to.equal(TradeStatus.OPEN) + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await trade.savedFillPrice()).to.equal(0) + expect(await rsrTrader.tradesOpen()).to.equal(1) + + // check balances + expect(await token0.balanceOf(trade.address)).to.equal(issueAmount.div(2000)) + expect(await rsr.balanceOf(trade.address)).to.equal(0) + + // Bid + // Advance time + await advanceToTimestamp((await trade.endTime()) - 5) + + // The trade cannot be settled yet + expect(await trade.canSettle()).to.equal(false) + + // Bid + if (bidType == BidType.CALLBACK) { + await (await ethers.getContractAt('ERC20Mock', await trade.buy())) + .connect(addr1) + .approve(router.address, constants.MaxUint256) + + await router.connect(addr1).bid(trade.address, addr1.address) + expect(await trade.bidder()).to.equal(router.address) + + // Noone can bid again on the trade directly + await expect( + trade.connect(addr1).bidWithCallback(new Uint8Array(0)) + ).to.be.revertedWith('bid already received') + + // Cannot bid once is settled via router + await expect( + router.connect(addr1).bid(trade.address, addr1.address) + ).to.be.revertedWith('trade not open') + + expect(await trade.bidType()).to.equal(BidType.CALLBACK) + } + + if (bidType == BidType.TRANSFER) { + await (await ethers.getContractAt('ERC20Mock', await trade.buy())) + .connect(addr1) + .approve(trade.address, constants.MaxUint256) + + await trade.connect(addr1).bid() + expect(await trade.bidder()).to.equal(addr1.address) + expect(await trade.bidType()).to.equal(BidType.TRANSFER) + } + + // Attempt to create trusted fill + await expect( + trade + .connect(addr1) + .createTrustedFill(cowSwapFillerMock.address, ethers.utils.randomBytes(32)) + ).to.be.revertedWith('bid already received') + + // Verify no trusted fill and trade is closed + expect(await trade.activeTrustedFill()).to.equal(ZERO_ADDRESS) + expect(await trade.status()).to.equal(TradeStatus.CLOSED) + expect(await rsrTrader.tradesOpen()).to.equal(0) + + // No sell tokens left on trade + expect(await token0.balanceOf(trade.address)).to.equal(0) + + // All funds moved to origin + expect(await token0.balanceOf(rsrTrader.address)).to.equal(0) + expect(await rsr.balanceOf(rsrTrader.address)).to.be.closeTo(0, 5000) + expect(await token0.balanceOf(trade.address)).to.equal(0) + expect(await rsr.balanceOf(trade.address)).to.equal(0) + }) }) }) }) diff --git a/yarn.lock b/yarn.lock index 8e6961b51..4a47c348b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1952,9 +1952,9 @@ __metadata: languageName: node linkType: hard -"@reserve-protocol/trusted-fillers@github:reserve-protocol/trusted-fillers#6ac118cc4a353e17712964d194814efce878363d": +"@reserve-protocol/trusted-fillers@github:reserve-protocol/trusted-fillers#a3fdf80204aa2915be313641590ff5c3be9a6c8e": version: 0.1.0 - resolution: "@reserve-protocol/trusted-fillers@https://github.com/reserve-protocol/trusted-fillers.git#commit=6ac118cc4a353e17712964d194814efce878363d" + resolution: "@reserve-protocol/trusted-fillers@https://github.com/reserve-protocol/trusted-fillers.git#commit=a3fdf80204aa2915be313641590ff5c3be9a6c8e" dependencies: "@cowprotocol/app-data": "npm:^2.5.1" "@cowprotocol/cow-sdk": "npm:^5.10.1" @@ -1964,7 +1964,7 @@ __metadata: ethers: "npm:^5.7.2" forge-std: "github:foundry-rs/forge-std#v1.9.7" viem: "npm:^2.23.11" - checksum: 10c0/c0efa66e0215872b69b39bd6176e5494ab78a4e053deae4b09540a00845ca1f723b68ea9271aca24ce222c9583c72f2287a81786b6e3f3df523615ce4917701b + checksum: 10c0/436a41237d9e1e1479c16c91991789d8153467371734cf7bad02301583500e26ee3cef318c7608197466b20093a735318f80573a7ff6d228729d0037e5e0090d languageName: node linkType: hard @@ -9872,7 +9872,7 @@ __metadata: "@openzeppelin/contracts": "npm:4.9.6" "@openzeppelin/contracts-upgradeable": "npm:4.9.6" "@openzeppelin/hardhat-upgrades": "npm:^1.23.0" - "@reserve-protocol/trusted-fillers": "github:reserve-protocol/trusted-fillers#6ac118cc4a353e17712964d194814efce878363d" + "@reserve-protocol/trusted-fillers": "github:reserve-protocol/trusted-fillers#a3fdf80204aa2915be313641590ff5c3be9a6c8e" "@safe-global/api-kit": "npm:^2.4.5" "@safe-global/protocol-kit": "npm:^4.1.0" "@tenderly/hardhat-tenderly": "npm:^1.7.7"