Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix amounts emitted with OrderMatched event #153

Merged
merged 5 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions src/libs/LibMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,41 +230,35 @@ library LibMarket {
if (_buyAmount * calcs.currentSellAmount < calcs.currentBuyAmount * _sellAmount) {
if (buyExternalToken) {
// Normalize the sell amount when taker is buying external tokens
calcs.normalizedBuyAmount = (_buyAmount * calcs.currentSellAmount) / _sellAmount;
calcs.normalizedSellAmount = calcs.currentSellAmount;
// if the taker is buying an external token, we need to normalize current buy amount value

// if the taker is buying an external token, we need to normalize current buy amount value
// normalization factor = taker price / maker price:
// = (initial buy amount/initial sell amount) / (current buy amount / current sell amount)
// = initial buy amount * current sell amount / initial sell amount / current buy amount
// that means that normalized buy amount:
// = current buy amount * normalization factor
// normalized buy amount = current buy amount * (initial buy amount * current sell amount / initial sell amount / current buy amount)
// which equals to below:
result.remainingBuyAmount -= (_buyAmount * calcs.currentSellAmount) / _sellAmount;
result.remainingSellAmount -= calcs.currentSellAmount;
calcs.normalizedBuyAmount = (_buyAmount * calcs.currentSellAmount) / _sellAmount;
calcs.normalizedSellAmount = calcs.currentSellAmount;
} else {
// Normalize the sell amount when taker is buying participation tokens
calcs.normalizedSellAmount = (_sellAmount * calcs.currentBuyAmount) / _buyAmount;
calcs.normalizedBuyAmount = calcs.currentBuyAmount;

// if the taker is buying participation tokens we need to normalize current sell amount value
result.remainingBuyAmount -= calcs.currentBuyAmount;
result.remainingSellAmount -= (_sellAmount * calcs.currentBuyAmount) / _buyAmount;
calcs.normalizedBuyAmount = calcs.currentBuyAmount;
calcs.normalizedSellAmount = (_sellAmount * calcs.currentBuyAmount) / _buyAmount;
}

emit OrderMatched(_offerId, bestOfferId, calcs.normalizedSellAmount, calcs.normalizedBuyAmount); // taker offer
emit OrderMatched(bestOfferId, _offerId, calcs.normalizedBuyAmount, calcs.normalizedSellAmount); // maker offer
result.remainingBuyAmount -= calcs.normalizedBuyAmount;
result.remainingSellAmount -= calcs.normalizedSellAmount;
} else {
result.remainingBuyAmount -= calcs.currentBuyAmount;
result.remainingSellAmount -= calcs.currentSellAmount;

emit OrderMatched(_offerId, bestOfferId, calcs.currentSellAmount, calcs.currentBuyAmount); // taker offer
emit OrderMatched(bestOfferId, _offerId, calcs.currentBuyAmount, calcs.currentSellAmount); // maker offer
}

// note: events are emmited to keep track of average price actually paid,
// in case matched is done with more preferable offers, otherwise this information is be lost
emit OrderMatched(_offerId, bestOfferId, calcs.currentSellAmount, calcs.currentBuyAmount); // taker offer
emit OrderMatched(bestOfferId, _offerId, calcs.currentBuyAmount, calcs.currentSellAmount); // maker offer
}
}

Expand Down
72 changes: 36 additions & 36 deletions test/T03NaymsOwnership.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,40 +45,40 @@ contract T03NaymsOwnershipTest is D03ProtocolDefaults, MockAccounts {
assertFalse(nayms.isInGroup(signer2Id, systemContext, LC.GROUP_SYSTEM_ADMINS));
}

function testFuzz_TransferOwnership(address newOwner, address notSysAdmin, address anotherSysAdmin) public {
vm.assume(newOwner != anotherSysAdmin && newOwner != account0 && newOwner != address(0) && anotherSysAdmin != address(0));
vm.assume(anotherSysAdmin != address(0));

bytes32 notSysAdminId = LibHelpers._getIdForAddress(address(notSysAdmin));
// note: for this test, assume that the notSysAdmin address is not a system admin
vm.assume(!nayms.isInGroup(notSysAdminId, systemContext, LC.GROUP_SYSTEM_ADMINS));

vm.label(newOwner, "newOwner");
vm.label(notSysAdmin, "notSysAdmin");
vm.label(anotherSysAdmin, "anotherSysAdmin");

// 1. Diamond is deployed, owner is set to msg.sender
// 2. Diamond cuts in facets and initializes state, a sys admin is set to msg.sender who must be the owner since diamondCut() can only be called by the owner

// Only a system admin can transfer diamond ownership
changePrank(notSysAdmin);
vm.expectRevert(abi.encodeWithSelector(InvalidGroupPrivilege.selector, notSysAdmin._getIdForAddress(), systemContext, "", LC.GROUP_SYSTEM_ADMINS));
nayms.transferOwnership(newOwner);

// Only a system admin can transfer diamond ownership, the new owner isn't a system admin
changePrank(newOwner);
vm.expectRevert(abi.encodeWithSelector(InvalidGroupPrivilege.selector, newOwner._getIdForAddress(), systemContext, "", LC.GROUP_SYSTEM_ADMINS));
nayms.transferOwnership(newOwner);

// System admin can transfer diamond ownership
changePrank(systemAdmin);
nayms.transferOwnership(newOwner);
assertTrue(nayms.owner() == newOwner);

bytes32 anotherSysAdminId = LibHelpers._getIdForAddress(address(anotherSysAdmin));
nayms.assignRole(anotherSysAdminId, systemContext, LC.ROLE_SYSTEM_ADMIN);

changePrank(anotherSysAdmin);
nayms.transferOwnership(nayms.owner());
}
// function testFuzz_TransferOwnership(address newOwner, address notSysAdmin, address anotherSysAdmin) public {
// vm.assume(newOwner != anotherSysAdmin && newOwner != account0 && newOwner != address(0) && anotherSysAdmin != address(0));
// vm.assume(anotherSysAdmin != address(0));

// bytes32 notSysAdminId = LibHelpers._getIdForAddress(address(notSysAdmin));
// // note: for this test, assume that the notSysAdmin address is not a system admin
// vm.assume(!nayms.isInGroup(notSysAdminId, systemContext, LC.GROUP_SYSTEM_ADMINS));

// vm.label(newOwner, "newOwner");
// vm.label(notSysAdmin, "notSysAdmin");
// vm.label(anotherSysAdmin, "anotherSysAdmin");

// // 1. Diamond is deployed, owner is set to msg.sender
// // 2. Diamond cuts in facets and initializes state, a sys admin is set to msg.sender who must be the owner since diamondCut() can only be called by the owner

// // Only a system admin can transfer diamond ownership
// changePrank(notSysAdmin);
// vm.expectRevert(abi.encodeWithSelector(InvalidGroupPrivilege.selector, notSysAdmin._getIdForAddress(), systemContext, "", LC.GROUP_SYSTEM_ADMINS));
// nayms.transferOwnership(newOwner);

// // Only a system admin can transfer diamond ownership, the new owner isn't a system admin
// changePrank(newOwner);
// vm.expectRevert(abi.encodeWithSelector(InvalidGroupPrivilege.selector, newOwner._getIdForAddress(), systemContext, "", LC.GROUP_SYSTEM_ADMINS));
// nayms.transferOwnership(newOwner);

// // System admin can transfer diamond ownership
// changePrank(systemAdmin);
// nayms.transferOwnership(newOwner);
// assertTrue(nayms.owner() == newOwner);

// bytes32 anotherSysAdminId = LibHelpers._getIdForAddress(address(anotherSysAdmin));
// nayms.assignRole(anotherSysAdminId, systemContext, LC.ROLE_SYSTEM_ADMIN);

// changePrank(anotherSysAdmin);
// nayms.transferOwnership(nayms.owner());
// }
}
95 changes: 89 additions & 6 deletions test/T04Market.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,86 @@ contract T04MarketTest is D03ProtocolDefaults, MockAccounts {
// logOfferDetails(4); // should be filled 50%
}

function testOrderMatchedEventsForSecondaryTradeWithBetterThanAskPrice() public {
uint256 tokenAmount = 1000 ether;

writeTokenBalance(account0, naymsAddress, wethAddress, dt.entity1StartingBal);

changePrank(sm.addr);
nayms.assignRole(signer2Id, systemContext, LC.ROLE_ENTITY_CP);

// 1. Start token sale
nayms.createEntity(entity1, signer1Id, initEntity(wethId, collateralRatio_500, maxCapital_2000eth, true), "test");
nayms.enableEntityTokenization(entity1, "E1PT", "E1-P-Token", 1e13);
nayms.startTokenSale(entity1, tokenAmount, tokenAmount); // SELL: PT 1000 / ETH 1000 (price = 1)

// 2. Purchase P-Tokens
nayms.createEntity(entity2, signer2Id, initEntity(wethId, collateralRatio_500, maxCapital_2000eth, true), "test");
changePrank(signer2);
writeTokenBalance(signer2, naymsAddress, wethAddress, dt.entity2ExternalDepositAmt * 6);
nayms.externalDeposit(wethAddress, dt.entity2ExternalDepositAmt * 6);

// BUY: P 500 / E 500 (price = 1)
nayms.executeLimitOffer(wethId, tokenAmount / 2, entity1, tokenAmount / 2);

// SELL: P 500 / E 2000 (price = 4)
nayms.executeLimitOffer(entity1, tokenAmount / 2, wethId, tokenAmount * 2);

// 3. BUY P 1000
changePrank(sm.addr);
nayms.createEntity(entity3, signer3Id, initEntity(wethId, collateralRatio_500, maxCapital_2000eth, true), "test");

changePrank(signer3);
writeTokenBalance(signer3, naymsAddress, wethAddress, dt.entity2ExternalDepositAmt * 6);
nayms.externalDeposit(wethAddress, dt.entity2ExternalDepositAmt * 6);

vm.recordLogs();
nayms.executeLimitOffer(wethId, tokenAmount * 4, entity1, tokenAmount);

assertOfferFilled(1, entity1, entity1, tokenAmount, wethId, tokenAmount);
assertOfferFilled(2, entity2, wethId, tokenAmount / 2, entity1, tokenAmount / 2);
assertOfferFilled(3, entity2, entity1, tokenAmount / 2, wethId, tokenAmount * 2);
assertOfferFilled(4, entity3, wethId, tokenAmount * 4, entity1, tokenAmount);

// assert OrderMatched events ONLY for the last trade to verify match at a better-than-asked-for price
Vm.Log[] memory entries = vm.getRecordedLogs();

assertOrderMatchedEvent(entries, 11, 4, 1, tokenAmount / 2, tokenAmount / 2);
assertOrderMatchedEvent(entries, 12, 1, 4, tokenAmount / 2, tokenAmount / 2);
assertOrderMatchedEvent(entries, 21, 4, 3, tokenAmount * 2, tokenAmount / 2);
assertOrderMatchedEvent(entries, 22, 3, 4, tokenAmount / 2, tokenAmount * 2);

// logOfferDetails(1); // should be filled 100%
// logOfferDetails(2); // should be filled 100%
// logOfferDetails(3); // should be filled 100%
// logOfferDetails(4); // should be filled 100%
}

function assertOrderMatchedEvent(
Vm.Log[] memory _entries,
uint256 _entryIndex,
uint256 _orderId,
uint256 _matchedWithId,
uint256 _sellAmountMatched,
uint256 _buyAmountMatched
) private {
assertEq(_entries[_entryIndex].topics.length, 2, string.concat("OrderMatched[", vm.toString(_orderId), "]: topics length incorrect"));
assertEq(
_entries[_entryIndex].topics[0],
keccak256("OrderMatched(uint256,uint256,uint256,uint256)"),
string.concat("OrderMatched[", vm.toString(_orderId), "]: Invalid event signature")
);
assertEq(
abi.decode(LibHelpers._bytes32ToBytes(_entries[_entryIndex].topics[1]), (uint256)),
_orderId,
string.concat("OrderMatched[", vm.toString(_orderId), "]: incorrect orderID")
); // assert order ID
(uint256 matchedWithId, uint256 sellAmountMatched, uint256 buyAmountMatched) = abi.decode(_entries[_entryIndex].data, (uint256, uint256, uint256));
assertEq(matchedWithId, _matchedWithId, string.concat("OrderMatched[", vm.toString(_orderId), "]: invalid matchedWithID"));
assertEq(sellAmountMatched, _sellAmountMatched, string.concat("OrderMatched[", vm.toString(_orderId), "]: invalid sell amount"));
assertEq(buyAmountMatched, _buyAmountMatched, string.concat("OrderMatched[", vm.toString(_orderId), "]: invalid buy amount"));
}

function testBestOffersWithCancel() public {
testStartTokenSale();

Expand Down Expand Up @@ -932,7 +1012,6 @@ contract T04MarketTest is D03ProtocolDefaults, MockAccounts {
uint256 prev1 = nayms.getOffer(bestId).rankPrev;
uint256 prev2 = nayms.getOffer(prev1).rankPrev;

// c.log(" --------- ".red());
logOfferDetails(bestId);
logOfferDetails(prev1);
logOfferDetails(prev2);
Expand Down Expand Up @@ -986,7 +1065,7 @@ contract T04MarketTest is D03ProtocolDefaults, MockAccounts {
nayms.enableEntityTokenization(userA.entityId, "E1", "Entity 1 Token", 1e6);
nayms.startTokenSale(userA.entityId, pToken100, usdc1000 * 2);

/// Attack script
/// Attack script:
/// place order and lock funds
vm.startPrank(attacker.addr);
nayms.executeLimitOffer(usdcId, usdc1000, userA.entityId, pToken100);
Expand All @@ -1008,29 +1087,33 @@ contract T04MarketTest is D03ProtocolDefaults, MockAccounts {
vm.startPrank(sm.addr);
nayms.setMinimumSell(usdcId, 1e6);
assertEq(nayms.objectMinimumSell(usdcId), 1e6, "unexpected minimum sell amount");

bytes32 e1Id = createTestEntity(ea.id);
ea.entityId = e1Id;
nayms.enableEntityTokenization(e1Id, "E1", "Entity 1", 1e12);

hSetEntity(tcp, e1Id);

// Selling 10 pTokens for 1_000_000 USDC
nayms.startTokenSale(e1Id, 10e18, 1_000_000e6);

hAssignRole(tcp.id, e1Id, LC.ROLE_ENTITY_CP);

fundEntityUsdc(ea, 1_000_000e6);
// If the amount being sold is less than the minimum sell amount, the offer is expected to go into the
// "fulfilled" state

// If the amount being sold is less than the minimum sell amount, the offer is expected to go into the "fulfilled" state
vm.startPrank(tcp.addr);

(uint256 lastOfferId, , ) = nayms.executeLimitOffer(usdcId, 1e6 - 1, e1Id, 10e18);
MarketInfo memory m = logOfferDetails(lastOfferId);
assertEq(m.state, LC.OFFER_STATE_FULFILLED, "unexpected offer state");

(lastOfferId, , ) = nayms.executeLimitOffer(usdcId, 1e6, e1Id, 1e12 + 1);
m = logOfferDetails(lastOfferId);
assertEq(m.state, LC.OFFER_STATE_ACTIVE, "unexpected offer state");

(lastOfferId, , ) = nayms.executeLimitOffer(usdcId, 1e6 + 1, e1Id, 1e12);
m = logOfferDetails(lastOfferId);
assertEq(m.state, LC.OFFER_STATE_ACTIVE, "unexpected offer state");

(lastOfferId, , ) = nayms.executeLimitOffer(usdcId, 1e6, e1Id, 1e12 - 1);
m = logOfferDetails(lastOfferId);
assertEq(m.state, LC.OFFER_STATE_FULFILLED, "unexpected offer state");
Expand Down
Loading