Forwarder Market sell specified amount or throw (#2521)
* Forwarder Market sell specified amount or throw * Address feedback comments * Break if we have only protocol fee remaining * Lint * Update deployed addresses * Updated artifacts and wrappers * [asset-swapper] Forwarder throws on market sell if amount not sold (#2534)
This commit is contained in:
parent
350feed993
commit
424cbd4831
@ -151,6 +151,72 @@ contract Forwarder is
|
|||||||
_unwrapAndTransferEth(wethRemaining);
|
_unwrapAndTransferEth(wethRemaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @dev Purchases as much of orders' makerAssets as possible by selling the specified amount of ETH
|
||||||
|
/// accounting for order and forwarder fees. This functions throws if ethSellAmount was not reached.
|
||||||
|
/// @param orders Array of order specifications used containing desired makerAsset and WETH as takerAsset.
|
||||||
|
/// @param ethSellAmount Desired amount of ETH to sell.
|
||||||
|
/// @param signatures Proofs that orders have been created by makers.
|
||||||
|
/// @param ethFeeAmounts Amounts of ETH, denominated in Wei, that are paid to corresponding feeRecipients.
|
||||||
|
/// @param feeRecipients Addresses that will receive ETH when orders are filled.
|
||||||
|
/// @return wethSpentAmount Amount of WETH spent on the given set of orders.
|
||||||
|
/// @return makerAssetAcquiredAmount Amount of maker asset acquired from the given set of orders.
|
||||||
|
function marketSellAmountWithEth(
|
||||||
|
LibOrder.Order[] memory orders,
|
||||||
|
uint256 ethSellAmount,
|
||||||
|
bytes[] memory signatures,
|
||||||
|
uint256[] memory ethFeeAmounts,
|
||||||
|
address payable[] memory feeRecipients
|
||||||
|
)
|
||||||
|
public
|
||||||
|
payable
|
||||||
|
returns (
|
||||||
|
uint256 wethSpentAmount,
|
||||||
|
uint256 makerAssetAcquiredAmount
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (ethSellAmount > msg.value) {
|
||||||
|
LibRichErrors.rrevert(LibForwarderRichErrors.CompleteSellFailedError(
|
||||||
|
ethSellAmount,
|
||||||
|
msg.value
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Pay ETH affiliate fees to all feeRecipient addresses
|
||||||
|
uint256 wethRemaining = _transferEthFeesAndWrapRemaining(
|
||||||
|
ethFeeAmounts,
|
||||||
|
feeRecipients
|
||||||
|
);
|
||||||
|
// Need enough remaining to ensure we can sell ethSellAmount
|
||||||
|
if (wethRemaining < ethSellAmount) {
|
||||||
|
LibRichErrors.rrevert(LibForwarderRichErrors.OverspentWethError(
|
||||||
|
wethRemaining,
|
||||||
|
ethSellAmount
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Spends up to ethSellAmount to fill orders, transfers purchased assets to msg.sender,
|
||||||
|
// and pays WETH order fees.
|
||||||
|
(
|
||||||
|
wethSpentAmount,
|
||||||
|
makerAssetAcquiredAmount
|
||||||
|
) = _marketSellExactAmountNoThrow(
|
||||||
|
orders,
|
||||||
|
ethSellAmount,
|
||||||
|
signatures
|
||||||
|
);
|
||||||
|
// Ensure we sold the specified amount (note: wethSpentAmount includes fees)
|
||||||
|
if (wethSpentAmount < ethSellAmount) {
|
||||||
|
LibRichErrors.rrevert(LibForwarderRichErrors.CompleteSellFailedError(
|
||||||
|
ethSellAmount,
|
||||||
|
wethSpentAmount
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate amount of WETH that hasn't been spent.
|
||||||
|
wethRemaining = wethRemaining.safeSub(wethSpentAmount);
|
||||||
|
|
||||||
|
// Refund remaining ETH to msg.sender.
|
||||||
|
_unwrapAndTransferEth(wethRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
/// @dev Attempt to buy makerAssetBuyAmount of makerAsset by selling ETH provided with transaction.
|
/// @dev Attempt to buy makerAssetBuyAmount of makerAsset by selling ETH provided with transaction.
|
||||||
/// The Forwarder may *fill* more than makerAssetBuyAmount of the makerAsset so that it can
|
/// The Forwarder may *fill* more than makerAssetBuyAmount of the makerAsset so that it can
|
||||||
/// pay takerFees where takerFeeAssetData == makerAssetData (i.e. percentage fees).
|
/// pay takerFees where takerFeeAssetData == makerAssetData (i.e. percentage fees).
|
||||||
|
@ -53,6 +53,7 @@ contract MixinExchangeWrapper {
|
|||||||
// ")"
|
// ")"
|
||||||
// )));
|
// )));
|
||||||
bytes4 constant public EXCHANGE_V2_ORDER_ID = 0x770501f8;
|
bytes4 constant public EXCHANGE_V2_ORDER_ID = 0x770501f8;
|
||||||
|
bytes4 constant internal ERC20_BRIDGE_PROXY_ID = 0xdc1600f3;
|
||||||
|
|
||||||
// solhint-disable var-name-mixedcase
|
// solhint-disable var-name-mixedcase
|
||||||
IExchange internal EXCHANGE;
|
IExchange internal EXCHANGE;
|
||||||
@ -73,6 +74,12 @@ contract MixinExchangeWrapper {
|
|||||||
EXCHANGE_V2 = IExchangeV2(_exchangeV2);
|
EXCHANGE_V2 = IExchangeV2(_exchangeV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SellFillResults {
|
||||||
|
uint256 wethSpentAmount;
|
||||||
|
uint256 makerAssetAcquiredAmount;
|
||||||
|
uint256 protocolFeePaid;
|
||||||
|
}
|
||||||
|
|
||||||
/// @dev Fills the input order.
|
/// @dev Fills the input order.
|
||||||
/// Returns false if the transaction would otherwise revert.
|
/// Returns false if the transaction would otherwise revert.
|
||||||
/// @param order Order struct containing order specifications.
|
/// @param order Order struct containing order specifications.
|
||||||
@ -115,11 +122,16 @@ contract MixinExchangeWrapper {
|
|||||||
uint256 remainingTakerAssetFillAmount
|
uint256 remainingTakerAssetFillAmount
|
||||||
)
|
)
|
||||||
internal
|
internal
|
||||||
returns (
|
returns (SellFillResults memory sellFillResults)
|
||||||
uint256 wethSpentAmount,
|
|
||||||
uint256 makerAssetAcquiredAmount
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
|
// If the maker asset is ERC20Bridge, take a snapshot of the Forwarder contract's balance.
|
||||||
|
bytes4 makerAssetProxyId = order.makerAssetData.readBytes4(0);
|
||||||
|
address tokenAddress;
|
||||||
|
uint256 balanceBefore;
|
||||||
|
if (makerAssetProxyId == ERC20_BRIDGE_PROXY_ID) {
|
||||||
|
tokenAddress = order.makerAssetData.readAddress(16);
|
||||||
|
balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this));
|
||||||
|
}
|
||||||
// No taker fee or percentage fee
|
// No taker fee or percentage fee
|
||||||
if (
|
if (
|
||||||
order.takerFee == 0 ||
|
order.takerFee == 0 ||
|
||||||
@ -132,11 +144,11 @@ contract MixinExchangeWrapper {
|
|||||||
signature
|
signature
|
||||||
);
|
);
|
||||||
|
|
||||||
wethSpentAmount = singleFillResults.takerAssetFilledAmount
|
sellFillResults.wethSpentAmount = singleFillResults.takerAssetFilledAmount;
|
||||||
.safeAdd(singleFillResults.protocolFeePaid);
|
sellFillResults.protocolFeePaid = singleFillResults.protocolFeePaid;
|
||||||
|
|
||||||
// Subtract fee from makerAssetFilledAmount for the net amount acquired.
|
// Subtract fee from makerAssetFilledAmount for the net amount acquired.
|
||||||
makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount
|
sellFillResults.makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount
|
||||||
.safeSub(singleFillResults.takerFeePaid);
|
.safeSub(singleFillResults.takerFeePaid);
|
||||||
|
|
||||||
// WETH fee
|
// WETH fee
|
||||||
@ -157,18 +169,27 @@ contract MixinExchangeWrapper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// WETH is also spent on the taker fee, so we add it here.
|
// WETH is also spent on the taker fee, so we add it here.
|
||||||
wethSpentAmount = singleFillResults.takerAssetFilledAmount
|
sellFillResults.wethSpentAmount = singleFillResults.takerAssetFilledAmount
|
||||||
.safeAdd(singleFillResults.takerFeePaid)
|
.safeAdd(singleFillResults.takerFeePaid);
|
||||||
.safeAdd(singleFillResults.protocolFeePaid);
|
sellFillResults.makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount;
|
||||||
|
sellFillResults.protocolFeePaid = singleFillResults.protocolFeePaid;
|
||||||
makerAssetAcquiredAmount = singleFillResults.makerAssetFilledAmount;
|
|
||||||
|
|
||||||
// Unsupported fee
|
// Unsupported fee
|
||||||
} else {
|
} else {
|
||||||
LibRichErrors.rrevert(LibForwarderRichErrors.UnsupportedFeeError(order.takerFeeAssetData));
|
LibRichErrors.rrevert(LibForwarderRichErrors.UnsupportedFeeError(order.takerFeeAssetData));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (wethSpentAmount, makerAssetAcquiredAmount);
|
// Account for the ERC20Bridge transfering more of the maker asset than expected.
|
||||||
|
if (makerAssetProxyId == ERC20_BRIDGE_PROXY_ID) {
|
||||||
|
uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this));
|
||||||
|
sellFillResults.makerAssetAcquiredAmount = LibSafeMath.max256(
|
||||||
|
balanceAfter.safeSub(balanceBefore),
|
||||||
|
sellFillResults.makerAssetAcquiredAmount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
order.makerAssetData.transferOut(sellFillResults.makerAssetAcquiredAmount);
|
||||||
|
return sellFillResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker.
|
/// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH has been sold by taker.
|
||||||
@ -189,7 +210,6 @@ contract MixinExchangeWrapper {
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
uint256 protocolFee = tx.gasprice.safeMul(EXCHANGE.protocolFeeMultiplier());
|
uint256 protocolFee = tx.gasprice.safeMul(EXCHANGE.protocolFeeMultiplier());
|
||||||
bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector;
|
|
||||||
|
|
||||||
for (uint256 i = 0; i != orders.length; i++) {
|
for (uint256 i = 0; i != orders.length; i++) {
|
||||||
// Preemptively skip to avoid division by zero in _marketSellSingleOrder
|
// Preemptively skip to avoid division by zero in _marketSellSingleOrder
|
||||||
@ -199,42 +219,27 @@ contract MixinExchangeWrapper {
|
|||||||
|
|
||||||
// The remaining amount of WETH to sell
|
// The remaining amount of WETH to sell
|
||||||
uint256 remainingTakerAssetFillAmount = wethSellAmount
|
uint256 remainingTakerAssetFillAmount = wethSellAmount
|
||||||
.safeSub(totalWethSpentAmount)
|
.safeSub(totalWethSpentAmount);
|
||||||
.safeSub(_isV2Order(orders[i]) ? 0 : protocolFee);
|
uint256 currentProtocolFee = _isV2Order(orders[i]) ? 0 : protocolFee;
|
||||||
|
if (remainingTakerAssetFillAmount > currentProtocolFee) {
|
||||||
// If the maker asset is ERC20Bridge, take a snapshot of the Forwarder contract's balance.
|
// Do not count the protocol fee as part of the fill amount.
|
||||||
bytes4 makerAssetProxyId = orders[i].makerAssetData.readBytes4(0);
|
remainingTakerAssetFillAmount = remainingTakerAssetFillAmount.safeSub(currentProtocolFee);
|
||||||
address tokenAddress;
|
} else {
|
||||||
uint256 balanceBefore;
|
// Stop if we don't have at least enough ETH to pay another protocol fee.
|
||||||
if (makerAssetProxyId == erc20BridgeProxyId) {
|
break;
|
||||||
tokenAddress = orders[i].makerAssetData.readAddress(16);
|
|
||||||
balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(
|
SellFillResults memory sellFillResults = _marketSellSingleOrder(
|
||||||
uint256 wethSpentAmount,
|
|
||||||
uint256 makerAssetAcquiredAmount
|
|
||||||
) = _marketSellSingleOrder(
|
|
||||||
orders[i],
|
orders[i],
|
||||||
signatures[i],
|
signatures[i],
|
||||||
remainingTakerAssetFillAmount
|
remainingTakerAssetFillAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
// Account for the ERC20Bridge transfering more of the maker asset than expected.
|
|
||||||
if (makerAssetProxyId == erc20BridgeProxyId) {
|
|
||||||
uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this));
|
|
||||||
makerAssetAcquiredAmount = LibSafeMath.max256(
|
|
||||||
balanceAfter.safeSub(balanceBefore),
|
|
||||||
makerAssetAcquiredAmount
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
orders[i].makerAssetData.transferOut(makerAssetAcquiredAmount);
|
|
||||||
|
|
||||||
totalWethSpentAmount = totalWethSpentAmount
|
totalWethSpentAmount = totalWethSpentAmount
|
||||||
.safeAdd(wethSpentAmount);
|
.safeAdd(sellFillResults.wethSpentAmount)
|
||||||
|
.safeAdd(sellFillResults.protocolFeePaid);
|
||||||
totalMakerAssetAcquiredAmount = totalMakerAssetAcquiredAmount
|
totalMakerAssetAcquiredAmount = totalMakerAssetAcquiredAmount
|
||||||
.safeAdd(makerAssetAcquiredAmount);
|
.safeAdd(sellFillResults.makerAssetAcquiredAmount);
|
||||||
|
|
||||||
// Stop execution if the entire amount of WETH has been sold
|
// Stop execution if the entire amount of WETH has been sold
|
||||||
if (totalWethSpentAmount >= wethSellAmount) {
|
if (totalWethSpentAmount >= wethSellAmount) {
|
||||||
@ -243,6 +248,56 @@ contract MixinExchangeWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @dev Synchronously executes multiple calls of fillOrder until total amount of WETH (exclusive of protocol fee)
|
||||||
|
/// has been sold by taker.
|
||||||
|
/// @param orders Array of order specifications.
|
||||||
|
/// @param wethSellAmount Desired amount of WETH to sell.
|
||||||
|
/// @param signatures Proofs that orders have been signed by makers.
|
||||||
|
/// @return totalWethSpentAmount Total amount of WETH spent on the given orders.
|
||||||
|
/// @return totalMakerAssetAcquiredAmount Total amount of maker asset acquired from the given orders.
|
||||||
|
function _marketSellExactAmountNoThrow(
|
||||||
|
LibOrder.Order[] memory orders,
|
||||||
|
uint256 wethSellAmount,
|
||||||
|
bytes[] memory signatures
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
returns (
|
||||||
|
uint256 totalWethSpentAmount,
|
||||||
|
uint256 totalMakerAssetAcquiredAmount
|
||||||
|
)
|
||||||
|
{
|
||||||
|
uint256 totalProtocolFeePaid;
|
||||||
|
|
||||||
|
for (uint256 i = 0; i != orders.length; i++) {
|
||||||
|
// Preemptively skip to avoid division by zero in _marketSellSingleOrder
|
||||||
|
if (orders[i].makerAssetAmount == 0 || orders[i].takerAssetAmount == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remaining amount of WETH to sell
|
||||||
|
uint256 remainingTakerAssetFillAmount = wethSellAmount
|
||||||
|
.safeSub(totalWethSpentAmount);
|
||||||
|
|
||||||
|
SellFillResults memory sellFillResults = _marketSellSingleOrder(
|
||||||
|
orders[i],
|
||||||
|
signatures[i],
|
||||||
|
remainingTakerAssetFillAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
totalWethSpentAmount = totalWethSpentAmount
|
||||||
|
.safeAdd(sellFillResults.wethSpentAmount);
|
||||||
|
totalMakerAssetAcquiredAmount = totalMakerAssetAcquiredAmount
|
||||||
|
.safeAdd(sellFillResults.makerAssetAcquiredAmount);
|
||||||
|
totalProtocolFeePaid = totalProtocolFeePaid.safeAdd(sellFillResults.protocolFeePaid);
|
||||||
|
|
||||||
|
// Stop execution if the entire amount of WETH has been sold
|
||||||
|
if (totalWethSpentAmount >= wethSellAmount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalWethSpentAmount = totalWethSpentAmount.safeAdd(totalProtocolFeePaid);
|
||||||
|
}
|
||||||
|
|
||||||
/// @dev Executes a single call of fillOrder according to the makerAssetBuyAmount and
|
/// @dev Executes a single call of fillOrder according to the makerAssetBuyAmount and
|
||||||
/// the amount already bought.
|
/// the amount already bought.
|
||||||
/// @param order A single order specification.
|
/// @param order A single order specification.
|
||||||
@ -338,8 +393,6 @@ contract MixinExchangeWrapper {
|
|||||||
uint256 totalMakerAssetAcquiredAmount
|
uint256 totalMakerAssetAcquiredAmount
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
bytes4 erc20BridgeProxyId = IAssetData(address(0)).ERC20Bridge.selector;
|
|
||||||
|
|
||||||
uint256 ordersLength = orders.length;
|
uint256 ordersLength = orders.length;
|
||||||
for (uint256 i = 0; i != ordersLength; i++) {
|
for (uint256 i = 0; i != ordersLength; i++) {
|
||||||
// Preemptively skip to avoid division by zero in _marketBuySingleOrder
|
// Preemptively skip to avoid division by zero in _marketBuySingleOrder
|
||||||
@ -354,7 +407,7 @@ contract MixinExchangeWrapper {
|
|||||||
bytes4 makerAssetProxyId = orders[i].makerAssetData.readBytes4(0);
|
bytes4 makerAssetProxyId = orders[i].makerAssetData.readBytes4(0);
|
||||||
address tokenAddress;
|
address tokenAddress;
|
||||||
uint256 balanceBefore;
|
uint256 balanceBefore;
|
||||||
if (makerAssetProxyId == erc20BridgeProxyId) {
|
if (makerAssetProxyId == ERC20_BRIDGE_PROXY_ID) {
|
||||||
tokenAddress = orders[i].makerAssetData.readAddress(16);
|
tokenAddress = orders[i].makerAssetData.readAddress(16);
|
||||||
balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this));
|
balanceBefore = IERC20Token(tokenAddress).balanceOf(address(this));
|
||||||
}
|
}
|
||||||
@ -369,7 +422,7 @@ contract MixinExchangeWrapper {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Account for the ERC20Bridge transfering more of the maker asset than expected.
|
// Account for the ERC20Bridge transfering more of the maker asset than expected.
|
||||||
if (makerAssetProxyId == erc20BridgeProxyId) {
|
if (makerAssetProxyId == ERC20_BRIDGE_PROXY_ID) {
|
||||||
uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this));
|
uint256 balanceAfter = IERC20Token(tokenAddress).balanceOf(address(this));
|
||||||
makerAssetAcquiredAmount = LibSafeMath.max256(
|
makerAssetAcquiredAmount = LibSafeMath.max256(
|
||||||
balanceAfter.safeSub(balanceBefore),
|
balanceAfter.safeSub(balanceBefore),
|
||||||
|
@ -29,6 +29,10 @@ library LibForwarderRichErrors {
|
|||||||
bytes4 internal constant COMPLETE_BUY_FAILED_ERROR_SELECTOR =
|
bytes4 internal constant COMPLETE_BUY_FAILED_ERROR_SELECTOR =
|
||||||
0x91353a0c;
|
0x91353a0c;
|
||||||
|
|
||||||
|
// bytes4(keccak256("CompleteSellFailedError(uint256,uint256)"))
|
||||||
|
bytes4 internal constant COMPLETE_SELL_FAILED_ERROR_SELECTOR =
|
||||||
|
0x450a0219;
|
||||||
|
|
||||||
// bytes4(keccak256("UnsupportedFeeError(bytes)"))
|
// bytes4(keccak256("UnsupportedFeeError(bytes)"))
|
||||||
bytes4 internal constant UNSUPPORTED_FEE_ERROR_SELECTOR =
|
bytes4 internal constant UNSUPPORTED_FEE_ERROR_SELECTOR =
|
||||||
0x31360af1;
|
0x31360af1;
|
||||||
@ -61,6 +65,21 @@ library LibForwarderRichErrors {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CompleteSellFailedError(
|
||||||
|
uint256 expectedAssetSellAmount,
|
||||||
|
uint256 actualAssetSellAmount
|
||||||
|
)
|
||||||
|
internal
|
||||||
|
pure
|
||||||
|
returns (bytes memory)
|
||||||
|
{
|
||||||
|
return abi.encodeWithSelector(
|
||||||
|
COMPLETE_SELL_FAILED_ERROR_SELECTOR,
|
||||||
|
expectedAssetSellAmount,
|
||||||
|
actualAssetSellAmount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function UnsupportedFeeError(
|
function UnsupportedFeeError(
|
||||||
bytes memory takerFeeAssetData
|
bytes memory takerFeeAssetData
|
||||||
)
|
)
|
||||||
|
@ -448,6 +448,24 @@ blockchainTests('Forwarder integration tests', env => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
blockchainTests.resets('marketSellAmountWithEth', () => {
|
||||||
|
it('should fail if the supplied amount is not sold', async () => {
|
||||||
|
const order = await maker.signOrderAsync();
|
||||||
|
const ethSellAmount = order.takerAssetAmount;
|
||||||
|
const revertError = new ExchangeForwarderRevertErrors.CompleteSellFailedError(
|
||||||
|
ethSellAmount,
|
||||||
|
order.takerAssetAmount.times(0.5).plus(DeploymentManager.protocolFee),
|
||||||
|
);
|
||||||
|
await testFactory.marketSellAmountTestAsync([order], ethSellAmount, 0.5, {
|
||||||
|
revertError,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should sell the supplied amount', async () => {
|
||||||
|
const order = await maker.signOrderAsync();
|
||||||
|
const ethSellAmount = order.takerAssetAmount;
|
||||||
|
await testFactory.marketSellAmountTestAsync([order], ethSellAmount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
blockchainTests.resets('marketBuyOrdersWithEth without extra fees', () => {
|
blockchainTests.resets('marketBuyOrdersWithEth without extra fees', () => {
|
||||||
it('should buy the exact amount of makerAsset in a single order', async () => {
|
it('should buy the exact amount of makerAsset in a single order', async () => {
|
||||||
const order = await maker.signOrderAsync({
|
const order = await maker.signOrderAsync({
|
||||||
|
@ -146,6 +146,51 @@ export class ForwarderTestFactory {
|
|||||||
await this._checkResultsAsync(txReceipt, orders, expectedOrderStatuses, expectedBalances);
|
await this._checkResultsAsync(txReceipt, orders, expectedOrderStatuses, expectedBalances);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async marketSellAmountTestAsync(
|
||||||
|
orders: SignedOrder[],
|
||||||
|
ethSellAmount: BigNumber,
|
||||||
|
fractionalNumberOfOrdersToFill: number,
|
||||||
|
options: Partial<MarketSellOptions> = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const orderInfoBefore = await Promise.all(
|
||||||
|
orders.map(order => this._deployment.exchange.getOrderInfo(order).callAsync()),
|
||||||
|
);
|
||||||
|
const expectedOrderStatuses = orderInfoBefore.map((orderInfo, i) =>
|
||||||
|
fractionalNumberOfOrdersToFill >= i + 1 && !(options.noopOrders || []).includes(i)
|
||||||
|
? OrderStatus.FullyFilled
|
||||||
|
: orderInfo.orderStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { balances: expectedBalances, wethSpentAmount } = await this._simulateForwarderFillAsync(
|
||||||
|
orders,
|
||||||
|
orderInfoBefore,
|
||||||
|
fractionalNumberOfOrdersToFill,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
const forwarderFeeAmounts = options.forwarderFeeAmounts || [];
|
||||||
|
const forwarderFeeRecipientAddresses = options.forwarderFeeRecipientAddresses || [];
|
||||||
|
|
||||||
|
const tx = this._forwarder
|
||||||
|
.marketSellAmountWithEth(
|
||||||
|
orders,
|
||||||
|
ethSellAmount,
|
||||||
|
orders.map(signedOrder => signedOrder.signature),
|
||||||
|
forwarderFeeAmounts,
|
||||||
|
forwarderFeeRecipientAddresses,
|
||||||
|
)
|
||||||
|
.awaitTransactionSuccessAsync({
|
||||||
|
value: wethSpentAmount.plus(BigNumber.sum(0, ...forwarderFeeAmounts)),
|
||||||
|
from: this._taker.address,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.revertError !== undefined) {
|
||||||
|
await expect(tx).to.revertWith(options.revertError);
|
||||||
|
} else {
|
||||||
|
const txReceipt = await tx;
|
||||||
|
await this._checkResultsAsync(txReceipt, orders, expectedOrderStatuses, expectedBalances);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _checkResultsAsync(
|
private async _checkResultsAsync(
|
||||||
txReceipt: TransactionReceiptWithDecodedLogs,
|
txReceipt: TransactionReceiptWithDecodedLogs,
|
||||||
|
@ -76,7 +76,13 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
.getABIEncodedTransactionData();
|
.getABIEncodedTransactionData();
|
||||||
} else {
|
} else {
|
||||||
calldataHexString = this._forwarder
|
calldataHexString = this._forwarder
|
||||||
.marketSellOrdersWithEth(orders, signatures, [feeAmount], [normalizedFeeRecipientAddress])
|
.marketSellAmountWithEth(
|
||||||
|
orders,
|
||||||
|
quote.takerAssetFillAmount,
|
||||||
|
signatures,
|
||||||
|
[feeAmount],
|
||||||
|
[normalizedFeeRecipientAddress],
|
||||||
|
)
|
||||||
.getABIEncodedTransactionData();
|
.getABIEncodedTransactionData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +145,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
txHash = await this._forwarder
|
txHash = await this._forwarder
|
||||||
.marketSellOrdersWithEth(orders, signatures, [feeAmount], [feeRecipient])
|
.marketSellAmountWithEth(orders, quote.takerAssetFillAmount, signatures, [feeAmount], [feeRecipient])
|
||||||
.sendTransactionAsync({
|
.sendTransactionAsync({
|
||||||
from: finalTakerAddress,
|
from: finalTakerAddress,
|
||||||
gas: gasLimit,
|
gas: gasLimit,
|
||||||
|
@ -25,6 +25,10 @@
|
|||||||
{
|
{
|
||||||
"note": "Add `dexForwarderBridge` addresses",
|
"note": "Add `dexForwarderBridge` addresses",
|
||||||
"pr": 2525
|
"pr": 2525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Redeploy `Forwarder` on all networks",
|
||||||
|
"pr": 2521
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"exchange": "0x61935cbdd02287b511119ddb11aeb42f1593b7ef",
|
"exchange": "0x61935cbdd02287b511119ddb11aeb42f1593b7ef",
|
||||||
"erc20Proxy": "0x95e6f48254609a6ee006f7d493c8e5fb97094cef",
|
"erc20Proxy": "0x95e6f48254609a6ee006f7d493c8e5fb97094cef",
|
||||||
"erc721Proxy": "0xefc70a1b18c432bdc64b596838b4d138f6bc6cad",
|
"erc721Proxy": "0xefc70a1b18c432bdc64b596838b4d138f6bc6cad",
|
||||||
"forwarder": "0x4aa817c6f383c8e8ae77301d18ce48efb16fd2be",
|
"forwarder": "0x6958f5e95332d93d21af0d7b9ca85b8212fee0a5",
|
||||||
"zrxToken": "0xe41d2489571d322189246dafa5ebde1f4699f498",
|
"zrxToken": "0xe41d2489571d322189246dafa5ebde1f4699f498",
|
||||||
"etherToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
"etherToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||||
"assetProxyOwner": "0xdffe798c7172dd6deb32baee68af322e8f495ce0",
|
"assetProxyOwner": "0xdffe798c7172dd6deb32baee68af322e8f495ce0",
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"exchange": "0xfb2dd2a1366de37f7241c83d47da58fd503e2c64",
|
"exchange": "0xfb2dd2a1366de37f7241c83d47da58fd503e2c64",
|
||||||
"assetProxyOwner": "0x0000000000000000000000000000000000000000",
|
"assetProxyOwner": "0x0000000000000000000000000000000000000000",
|
||||||
"zeroExGovernor": "0x53993733d41a88ae86f77a18a024e5548ee26579",
|
"zeroExGovernor": "0x53993733d41a88ae86f77a18a024e5548ee26579",
|
||||||
"forwarder": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf",
|
"forwarder": "0x2127a60bedfba1c01857b09b8f24094049c48493",
|
||||||
"coordinatorRegistry": "0x403cc23e88c17c4652fb904784d1af640a6722d9",
|
"coordinatorRegistry": "0x403cc23e88c17c4652fb904784d1af640a6722d9",
|
||||||
"coordinator": "0x6ff734d96104965c9c1b0108f83abc46e6e501df",
|
"coordinator": "0x6ff734d96104965c9c1b0108f83abc46e6e501df",
|
||||||
"multiAssetProxy": "0xab8fbd189c569ccdee3a4d929bb7f557be4028f6",
|
"multiAssetProxy": "0xab8fbd189c569ccdee3a4d929bb7f557be4028f6",
|
||||||
@ -74,7 +74,7 @@
|
|||||||
"etherToken": "0xc778417e063141139fce010982780140aa0cd5ab",
|
"etherToken": "0xc778417e063141139fce010982780140aa0cd5ab",
|
||||||
"assetProxyOwner": "0x0000000000000000000000000000000000000000",
|
"assetProxyOwner": "0x0000000000000000000000000000000000000000",
|
||||||
"zeroExGovernor": "0x3f46b98061a3e1e1f41dff296ec19402c298f8a9",
|
"zeroExGovernor": "0x3f46b98061a3e1e1f41dff296ec19402c298f8a9",
|
||||||
"forwarder": "0x263ccc190ccb1cb3342ab07e50f03edb2f05aa36",
|
"forwarder": "0x18571835c95a6d79b2f5c45b676ccd16f5fa34a1",
|
||||||
"coordinatorRegistry": "0x1084b6a398e47907bae43fec3ff4b677db6e4fee",
|
"coordinatorRegistry": "0x1084b6a398e47907bae43fec3ff4b677db6e4fee",
|
||||||
"coordinator": "0x70c5385ee5ee4629ef72abd169e888c8b4a12238",
|
"coordinator": "0x70c5385ee5ee4629ef72abd169e888c8b4a12238",
|
||||||
"multiAssetProxy": "0xb34cde0ad3a83d04abebc0b66e75196f22216621",
|
"multiAssetProxy": "0xb34cde0ad3a83d04abebc0b66e75196f22216621",
|
||||||
@ -107,7 +107,7 @@
|
|||||||
"exchange": "0x4eacd0af335451709e1e7b570b8ea68edec8bc97",
|
"exchange": "0x4eacd0af335451709e1e7b570b8ea68edec8bc97",
|
||||||
"assetProxyOwner": "0x0000000000000000000000000000000000000000",
|
"assetProxyOwner": "0x0000000000000000000000000000000000000000",
|
||||||
"zeroExGovernor": "0x6ff734d96104965c9c1b0108f83abc46e6e501df",
|
"zeroExGovernor": "0x6ff734d96104965c9c1b0108f83abc46e6e501df",
|
||||||
"forwarder": "0x263ccc190ccb1cb3342ab07e50f03edb2f05aa36",
|
"forwarder": "0x01c0ecf5d1a22de07a2de84c322bfa2b5435990e",
|
||||||
"coordinatorRegistry": "0x09fb99968c016a3ff537bf58fb3d9fe55a7975d5",
|
"coordinatorRegistry": "0x09fb99968c016a3ff537bf58fb3d9fe55a7975d5",
|
||||||
"coordinator": "0xd29e59e51e8ab5f94121efaeebd935ca4214e257",
|
"coordinator": "0xd29e59e51e8ab5f94121efaeebd935ca4214e257",
|
||||||
"multiAssetProxy": "0xf6313a772c222f51c28f2304c0703b8cf5428fd8",
|
"multiAssetProxy": "0xf6313a772c222f51c28f2304c0703b8cf5428fd8",
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
{
|
{
|
||||||
"note": "Added `MaximumGasPrice` artifact",
|
"note": "Added `MaximumGasPrice` artifact",
|
||||||
"pr": 2511
|
"pr": 2511
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Added `Forwarder.marketSellAmountWithEth`",
|
||||||
|
"pr": 2521
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
55
packages/contract-artifacts/artifacts/Forwarder.json
generated
55
packages/contract-artifacts/artifacts/Forwarder.json
generated
File diff suppressed because one or more lines are too long
@ -9,6 +9,10 @@
|
|||||||
{
|
{
|
||||||
"note": "Added wrapper for MaximumGasPrice",
|
"note": "Added wrapper for MaximumGasPrice",
|
||||||
"pr": 2511
|
"pr": 2511
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Added `Forwarder.marketSellAmountWithEth`",
|
||||||
|
"pr": 2521
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -392,6 +392,103 @@ export class ForwarderContract extends BaseContract {
|
|||||||
stateMutability: 'payable',
|
stateMutability: 'payable',
|
||||||
type: 'function',
|
type: 'function',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
constant: false,
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: 'orders',
|
||||||
|
type: 'tuple[]',
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
name: 'makerAddress',
|
||||||
|
type: 'address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'takerAddress',
|
||||||
|
type: 'address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feeRecipientAddress',
|
||||||
|
type: 'address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'senderAddress',
|
||||||
|
type: 'address',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'makerAssetAmount',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'takerAssetAmount',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'makerFee',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'takerFee',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expirationTimeSeconds',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'salt',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'makerAssetData',
|
||||||
|
type: 'bytes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'takerAssetData',
|
||||||
|
type: 'bytes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'makerFeeAssetData',
|
||||||
|
type: 'bytes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'takerFeeAssetData',
|
||||||
|
type: 'bytes',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ethSellAmount',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'signatures',
|
||||||
|
type: 'bytes[]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ethFeeAmounts',
|
||||||
|
type: 'uint256[]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'feeRecipients',
|
||||||
|
type: 'address[]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: 'marketSellAmountWithEth',
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
name: 'wethSpentAmount',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'makerAssetAcquiredAmount',
|
||||||
|
type: 'uint256',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
payable: true,
|
||||||
|
stateMutability: 'payable',
|
||||||
|
type: 'function',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
constant: false,
|
constant: false,
|
||||||
inputs: [
|
inputs: [
|
||||||
@ -891,6 +988,100 @@ export class ForwarderContract extends BaseContract {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Purchases as much of orders' makerAssets as possible by selling the specified amount of ETH
|
||||||
|
* accounting for order and forwarder fees. This functions throws if ethSellAmount was not reached.
|
||||||
|
* @param orders Array of order specifications used containing desired
|
||||||
|
* makerAsset and WETH as takerAsset.
|
||||||
|
* @param ethSellAmount Desired amount of ETH to sell.
|
||||||
|
* @param signatures Proofs that orders have been created by makers.
|
||||||
|
* @param ethFeeAmounts Amounts of ETH, denominated in Wei, that are paid to
|
||||||
|
* corresponding feeRecipients.
|
||||||
|
* @param feeRecipients Addresses that will receive ETH when orders are filled.
|
||||||
|
* @returns wethSpentAmount Amount of WETH spent on the given set of orders.makerAssetAcquiredAmount Amount of maker asset acquired from the given set of orders.
|
||||||
|
*/
|
||||||
|
public marketSellAmountWithEth(
|
||||||
|
orders: Array<{
|
||||||
|
makerAddress: string;
|
||||||
|
takerAddress: string;
|
||||||
|
feeRecipientAddress: string;
|
||||||
|
senderAddress: string;
|
||||||
|
makerAssetAmount: BigNumber;
|
||||||
|
takerAssetAmount: BigNumber;
|
||||||
|
makerFee: BigNumber;
|
||||||
|
takerFee: BigNumber;
|
||||||
|
expirationTimeSeconds: BigNumber;
|
||||||
|
salt: BigNumber;
|
||||||
|
makerAssetData: string;
|
||||||
|
takerAssetData: string;
|
||||||
|
makerFeeAssetData: string;
|
||||||
|
takerFeeAssetData: string;
|
||||||
|
}>,
|
||||||
|
ethSellAmount: BigNumber,
|
||||||
|
signatures: string[],
|
||||||
|
ethFeeAmounts: BigNumber[],
|
||||||
|
feeRecipients: string[],
|
||||||
|
): ContractTxFunctionObj<[BigNumber, BigNumber]> {
|
||||||
|
const self = (this as any) as ForwarderContract;
|
||||||
|
assert.isArray('orders', orders);
|
||||||
|
assert.isBigNumber('ethSellAmount', ethSellAmount);
|
||||||
|
assert.isArray('signatures', signatures);
|
||||||
|
assert.isArray('ethFeeAmounts', ethFeeAmounts);
|
||||||
|
assert.isArray('feeRecipients', feeRecipients);
|
||||||
|
const functionSignature =
|
||||||
|
'marketSellAmountWithEth((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],uint256,bytes[],uint256[],address[])';
|
||||||
|
|
||||||
|
return {
|
||||||
|
async sendTransactionAsync(
|
||||||
|
txData?: Partial<TxData> | undefined,
|
||||||
|
opts: SendTransactionOpts = { shouldValidate: true },
|
||||||
|
): Promise<string> {
|
||||||
|
const txDataWithDefaults = await self._applyDefaultsToTxDataAsync(
|
||||||
|
{ ...txData, data: this.getABIEncodedTransactionData() },
|
||||||
|
this.estimateGasAsync.bind(this),
|
||||||
|
);
|
||||||
|
if (opts.shouldValidate !== false) {
|
||||||
|
await this.callAsync(txDataWithDefaults);
|
||||||
|
}
|
||||||
|
return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults);
|
||||||
|
},
|
||||||
|
awaitTransactionSuccessAsync(
|
||||||
|
txData?: Partial<TxData>,
|
||||||
|
opts: AwaitTransactionSuccessOpts = { shouldValidate: true },
|
||||||
|
): PromiseWithTransactionHash<TransactionReceiptWithDecodedLogs> {
|
||||||
|
return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts);
|
||||||
|
},
|
||||||
|
async estimateGasAsync(txData?: Partial<TxData> | undefined): Promise<number> {
|
||||||
|
const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({
|
||||||
|
...txData,
|
||||||
|
data: this.getABIEncodedTransactionData(),
|
||||||
|
});
|
||||||
|
return self._web3Wrapper.estimateGasAsync(txDataWithDefaults);
|
||||||
|
},
|
||||||
|
async callAsync(
|
||||||
|
callData: Partial<CallData> = {},
|
||||||
|
defaultBlock?: BlockParam,
|
||||||
|
): Promise<[BigNumber, BigNumber]> {
|
||||||
|
BaseContract._assertCallParams(callData, defaultBlock);
|
||||||
|
const rawCallResult = await self._performCallAsync(
|
||||||
|
{ ...callData, data: this.getABIEncodedTransactionData() },
|
||||||
|
defaultBlock,
|
||||||
|
);
|
||||||
|
const abiEncoder = self._lookupAbiEncoder(functionSignature);
|
||||||
|
BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder);
|
||||||
|
return abiEncoder.strictDecodeReturnValue<[BigNumber, BigNumber]>(rawCallResult);
|
||||||
|
},
|
||||||
|
getABIEncodedTransactionData(): string {
|
||||||
|
return self._strictEncodeArguments(functionSignature, [
|
||||||
|
orders,
|
||||||
|
ethSellAmount,
|
||||||
|
signatures,
|
||||||
|
ethFeeAmounts,
|
||||||
|
feeRecipients,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Purchases as much of orders' makerAssets as possible by selling as much of the ETH value sent
|
* Purchases as much of orders' makerAssets as possible by selling as much of the ETH value sent
|
||||||
* as possible, accounting for order and forwarder fees.
|
* as possible, accounting for order and forwarder fees.
|
||||||
|
@ -22,6 +22,19 @@ export class CompleteBuyFailedError extends RevertError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CompleteSellFailedError extends RevertError {
|
||||||
|
constructor(
|
||||||
|
expectedAssetSellAmount?: BigNumber | number | string,
|
||||||
|
actualAssetSellAmount?: BigNumber | number | string,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
'CompleteSellFailedError',
|
||||||
|
'CompleteSellFailedError(uint256 expectedAssetSellAmount, uint256 actualAssetSellAmount)',
|
||||||
|
{ expectedAssetSellAmount, actualAssetSellAmount },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class UnsupportedFeeError extends RevertError {
|
export class UnsupportedFeeError extends RevertError {
|
||||||
constructor(takerFeeAssetData?: string) {
|
constructor(takerFeeAssetData?: string) {
|
||||||
super('UnsupportedFeeError', 'UnsupportedFeeError(bytes takerFeeAssetData)', { takerFeeAssetData });
|
super('UnsupportedFeeError', 'UnsupportedFeeError(bytes takerFeeAssetData)', { takerFeeAssetData });
|
||||||
@ -46,6 +59,7 @@ export class MsgValueCannotEqualZeroError extends RevertError {
|
|||||||
const types = [
|
const types = [
|
||||||
UnregisteredAssetProxyError,
|
UnregisteredAssetProxyError,
|
||||||
CompleteBuyFailedError,
|
CompleteBuyFailedError,
|
||||||
|
CompleteSellFailedError,
|
||||||
UnsupportedFeeError,
|
UnsupportedFeeError,
|
||||||
OverspentWethError,
|
OverspentWethError,
|
||||||
MsgValueCannotEqualZeroError,
|
MsgValueCannotEqualZeroError,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user