diff --git a/packages/hebao_v2/README.md b/packages/hebao_v2/README.md index 00ecda1e5..25986f59d 100644 --- a/packages/hebao_v2/README.md +++ b/packages/hebao_v2/README.md @@ -3,17 +3,20 @@ _[hebao]_(荷包)means wallet in China -- see https://www.pinterest.com/pin/373376625330954965 for examples. # Build + ``` yarn install yarn compile ``` # Run test + ``` yarn test ``` # Deploy contract to arbitrum: + ``` npx hardhat run --network arbitrum scripts/deploy-arbitrum.ts ``` diff --git a/packages/hebao_v2/hardhat.config.ts b/packages/hebao_v2/hardhat.config.ts index 7f72504ff..26e9e877d 100644 --- a/packages/hebao_v2/hardhat.config.ts +++ b/packages/hebao_v2/hardhat.config.ts @@ -68,7 +68,9 @@ export default { gasMultiplier: 1, timeout: 20000, httpHeaders: undefined, - accounts: loadTestAccounts().map(item => item.privateKey).slice() + accounts: loadTestAccounts() + .map(item => item.privateKey) + .slice() } }, diff --git a/packages/hebao_v2/script/deploy-arbitrum.ts b/packages/hebao_v2/script/deploy-arbitrum.ts index 0bf7fa586..11a2f3f0e 100644 --- a/packages/hebao_v2/script/deploy-arbitrum.ts +++ b/packages/hebao_v2/script/deploy-arbitrum.ts @@ -1,9 +1,6 @@ const hre = require("hardhat"); const ethers = hre.ethers; -import { - newWalletImpl, - newWalletFactoryContract -} from "../test/commons"; +import { newWalletImpl, newWalletFactoryContract } from "../test/commons"; import { signCreateWallet } from "../test/helper/signatureUtils"; import BN = require("bn.js"); @@ -37,9 +34,9 @@ async function newWallet() { signature: Buffer.from(signature.txSignature.slice(2), "hex") }; - const walletFactory = await (await ethers.getContractFactory( - "WalletFactory" - )).attach(walletFactoryAddress); + const walletFactory = await ( + await ethers.getContractFactory("WalletFactory") + ).attach(walletFactoryAddress); const walletAddrComputed = await walletFactory.computeWalletAddress( ownerAddr, @@ -47,7 +44,9 @@ async function newWallet() { ); console.log("walletAddrcomputed:", walletAddrComputed); - const tx = await walletFactory.createWallet(walletConfig, salt, { gasLimit:10000000 }); + const tx = await walletFactory.createWallet(walletConfig, salt, { + gasLimit: 10000000 + }); console.log("tx:", tx); const receipt = await tx.wait(); console.log("receipt:", receipt); diff --git a/packages/hebao_v2/test/callContract.spec.ts b/packages/hebao_v2/test/callContract.spec.ts index 83d2b81be..eef3358ce 100644 --- a/packages/hebao_v2/test/callContract.spec.ts +++ b/packages/hebao_v2/test/callContract.spec.ts @@ -23,9 +23,9 @@ describe("wallet", () => { account2 ); LRC = await (await ethers.getContractFactory("LRC")).deploy(); - TestContract = await (await ethers.getContractFactory( - "TestTargetContract" - )).deploy(); + TestContract = await ( + await ethers.getContractFactory("TestTargetContract") + ).deploy(); }); describe("callContract", () => { diff --git a/packages/hebao_v2/test/commons.ts b/packages/hebao_v2/test/commons.ts index b0fb1064d..0a73b65d8 100644 --- a/packages/hebao_v2/test/commons.ts +++ b/packages/hebao_v2/test/commons.ts @@ -7,53 +7,61 @@ import BN = require("bn.js"); import { signCreateWallet } from "./helper/signatureUtils"; export async function newWalletImpl() { - const ERC1271Lib = await (await ethers.getContractFactory( - "ERC1271Lib" - )).deploy(); + const ERC1271Lib = await ( + await ethers.getContractFactory("ERC1271Lib") + ).deploy(); const ERC20Lib = await (await ethers.getContractFactory("ERC20Lib")).deploy(); - const GuardianLib = await (await ethers.getContractFactory( - "GuardianLib" - )).deploy(); - const InheritanceLib = await (await ethers.getContractFactory( - "InheritanceLib" - )).deploy(); - const LockLib = await (await ethers.getContractFactory("LockLib", { - libraries: { - GuardianLib: GuardianLib.address - } - })).deploy(); - const MetaTxLib = await (await ethers.getContractFactory("MetaTxLib", { - libraries: { - ERC20Lib: ERC20Lib.address - } - })).deploy(); + const GuardianLib = await ( + await ethers.getContractFactory("GuardianLib") + ).deploy(); + const InheritanceLib = await ( + await ethers.getContractFactory("InheritanceLib") + ).deploy(); + const LockLib = await ( + await ethers.getContractFactory("LockLib", { + libraries: { + GuardianLib: GuardianLib.address + } + }) + ).deploy(); + const MetaTxLib = await ( + await ethers.getContractFactory("MetaTxLib", { + libraries: { + ERC20Lib: ERC20Lib.address + } + }) + ).deploy(); const QuotaLib = await (await ethers.getContractFactory("QuotaLib")).deploy(); - const RecoverLib = await (await ethers.getContractFactory("RecoverLib", { - libraries: { - GuardianLib: GuardianLib.address - } - })).deploy(); - const UpgradeLib = await (await ethers.getContractFactory( - "UpgradeLib" - )).deploy(); - const WhitelistLib = await (await ethers.getContractFactory( - "WhitelistLib" - )).deploy(); + const RecoverLib = await ( + await ethers.getContractFactory("RecoverLib", { + libraries: { + GuardianLib: GuardianLib.address + } + }) + ).deploy(); + const UpgradeLib = await ( + await ethers.getContractFactory("UpgradeLib") + ).deploy(); + const WhitelistLib = await ( + await ethers.getContractFactory("WhitelistLib") + ).deploy(); - const smartWallet = await (await ethers.getContractFactory("SmartWallet", { - libraries: { - ERC1271Lib: ERC1271Lib.address, - ERC20Lib: ERC20Lib.address, - GuardianLib: GuardianLib.address, - InheritanceLib: InheritanceLib.address, - LockLib: LockLib.address, - MetaTxLib: MetaTxLib.address, - QuotaLib: QuotaLib.address, - RecoverLib: RecoverLib.address, - UpgradeLib: UpgradeLib.address, - WhitelistLib: WhitelistLib.address - } - })).deploy(ethers.constants.AddressZero); + const smartWallet = await ( + await ethers.getContractFactory("SmartWallet", { + libraries: { + ERC1271Lib: ERC1271Lib.address, + ERC20Lib: ERC20Lib.address, + GuardianLib: GuardianLib.address, + InheritanceLib: InheritanceLib.address, + LockLib: LockLib.address, + MetaTxLib: MetaTxLib.address, + QuotaLib: QuotaLib.address, + RecoverLib: RecoverLib.address, + UpgradeLib: UpgradeLib.address, + WhitelistLib: WhitelistLib.address + } + }) + ).deploy(ethers.constants.AddressZero); return smartWallet; } @@ -63,62 +71,70 @@ export async function newWalletFactoryContract(deployer?: string) { let smartWallet: Contract; let walletFactory: Contract; - testPriceOracle = await (await ethers.getContractFactory( - "TestPriceOracle" - )).deploy(); + testPriceOracle = await ( + await ethers.getContractFactory("TestPriceOracle") + ).deploy(); - const ERC1271Lib = await (await ethers.getContractFactory( - "ERC1271Lib" - )).deploy(); + const ERC1271Lib = await ( + await ethers.getContractFactory("ERC1271Lib") + ).deploy(); const ERC20Lib = await (await ethers.getContractFactory("ERC20Lib")).deploy(); - const GuardianLib = await (await ethers.getContractFactory( - "GuardianLib" - )).deploy(); - const InheritanceLib = await (await ethers.getContractFactory( - "InheritanceLib" - )).deploy(); - const LockLib = await (await ethers.getContractFactory("LockLib", { - libraries: { - GuardianLib: GuardianLib.address - } - })).deploy(); - const MetaTxLib = await (await ethers.getContractFactory("MetaTxLib", { - libraries: { - ERC20Lib: ERC20Lib.address - } - })).deploy(); + const GuardianLib = await ( + await ethers.getContractFactory("GuardianLib") + ).deploy(); + const InheritanceLib = await ( + await ethers.getContractFactory("InheritanceLib") + ).deploy(); + const LockLib = await ( + await ethers.getContractFactory("LockLib", { + libraries: { + GuardianLib: GuardianLib.address + } + }) + ).deploy(); + const MetaTxLib = await ( + await ethers.getContractFactory("MetaTxLib", { + libraries: { + ERC20Lib: ERC20Lib.address + } + }) + ).deploy(); const QuotaLib = await (await ethers.getContractFactory("QuotaLib")).deploy(); - const RecoverLib = await (await ethers.getContractFactory("RecoverLib", { - libraries: { - GuardianLib: GuardianLib.address - } - })).deploy(); - const UpgradeLib = await (await ethers.getContractFactory( - "UpgradeLib" - )).deploy(); - const WhitelistLib = await (await ethers.getContractFactory( - "WhitelistLib" - )).deploy(); + const RecoverLib = await ( + await ethers.getContractFactory("RecoverLib", { + libraries: { + GuardianLib: GuardianLib.address + } + }) + ).deploy(); + const UpgradeLib = await ( + await ethers.getContractFactory("UpgradeLib") + ).deploy(); + const WhitelistLib = await ( + await ethers.getContractFactory("WhitelistLib") + ).deploy(); - smartWallet = await (await ethers.getContractFactory("SmartWallet", { - libraries: { - ERC1271Lib: ERC1271Lib.address, - ERC20Lib: ERC20Lib.address, - GuardianLib: GuardianLib.address, - InheritanceLib: InheritanceLib.address, - LockLib: LockLib.address, - MetaTxLib: MetaTxLib.address, - QuotaLib: QuotaLib.address, - RecoverLib: RecoverLib.address, - UpgradeLib: UpgradeLib.address, - WhitelistLib: WhitelistLib.address - } - })).deploy(ethers.constants.AddressZero /*testPriceOracle.address*/); + smartWallet = await ( + await ethers.getContractFactory("SmartWallet", { + libraries: { + ERC1271Lib: ERC1271Lib.address, + ERC20Lib: ERC20Lib.address, + GuardianLib: GuardianLib.address, + InheritanceLib: InheritanceLib.address, + LockLib: LockLib.address, + MetaTxLib: MetaTxLib.address, + QuotaLib: QuotaLib.address, + RecoverLib: RecoverLib.address, + UpgradeLib: UpgradeLib.address, + WhitelistLib: WhitelistLib.address + } + }) + ).deploy(ethers.constants.AddressZero /*testPriceOracle.address*/); console.log("smartWallet address:", smartWallet.address); - walletFactory = await (await ethers.getContractFactory( - "WalletFactory" - )).deploy(smartWallet.address); + walletFactory = await ( + await ethers.getContractFactory("WalletFactory") + ).deploy(smartWallet.address); await walletFactory.deployed(); @@ -171,29 +187,28 @@ export async function newWallet( // const allEvents = await getAllEvent(walletFactory, tx.blockNumber); // console.log(allEvents); - const smartWallet = await (await ethers.getContractFactory("SmartWallet", { - libraries: { - ERC1271Lib: ethers.constants.AddressZero, - ERC20Lib: ethers.constants.AddressZero, - GuardianLib: ethers.constants.AddressZero, - InheritanceLib: ethers.constants.AddressZero, - LockLib: ethers.constants.AddressZero, - MetaTxLib: ethers.constants.AddressZero, - QuotaLib: ethers.constants.AddressZero, - RecoverLib: ethers.constants.AddressZero, - UpgradeLib: ethers.constants.AddressZero, - WhitelistLib: ethers.constants.AddressZero - } - })).attach(walletAddrComputed); + const smartWallet = await ( + await ethers.getContractFactory("SmartWallet", { + libraries: { + ERC1271Lib: ethers.constants.AddressZero, + ERC20Lib: ethers.constants.AddressZero, + GuardianLib: ethers.constants.AddressZero, + InheritanceLib: ethers.constants.AddressZero, + LockLib: ethers.constants.AddressZero, + MetaTxLib: ethers.constants.AddressZero, + QuotaLib: ethers.constants.AddressZero, + RecoverLib: ethers.constants.AddressZero, + UpgradeLib: ethers.constants.AddressZero, + WhitelistLib: ethers.constants.AddressZero + } + }) + ).attach(walletAddrComputed); // console.log("SmartWallet:", smartWallet); return smartWallet; } -export async function getAllEvent( - contract: any, - fromBlock: number -) { +export async function getAllEvent(contract: any, fromBlock: number) { const events = await contract.queryFilter( { address: contract.address }, fromBlock @@ -250,9 +265,12 @@ export async function getContractABI(contractName: string) { }); } -export function sortSignersAndSignatures(signers: string[], signatures: Buffer[]) { +export function sortSignersAndSignatures( + signers: string[], + signatures: Buffer[] +) { const sigMap = new Map(); - signers.forEach(function(signer, i){ + signers.forEach(function(signer, i) { sigMap.set(signer, signatures[i]); }); diff --git a/packages/hebao_v2/test/helper/signatureUtils.ts b/packages/hebao_v2/test/helper/signatureUtils.ts index f94671927..30f394b2a 100644 --- a/packages/hebao_v2/test/helper/signatureUtils.ts +++ b/packages/hebao_v2/test/helper/signatureUtils.ts @@ -79,22 +79,13 @@ export function signChangeMasterCopy( signer: string ) { const domainSeprator = eip712.hash("LoopringWallet", "2.0.0", masterCopy); - const TYPE_STR = "changeMasterCopy(address wallet,uint256 validUntil,address masterCopy)"; + const TYPE_STR = + "changeMasterCopy(address wallet,uint256 validUntil,address masterCopy)"; const CREATE_WALLET_TYPEHASH = ethUtil.keccak(Buffer.from(TYPE_STR)); const encodedRequest = ethAbi.encodeParameters( - [ - "bytes32", - "address", - "uint256", - "address" - ], - [ - CREATE_WALLET_TYPEHASH, - walletAddress, - validUntil, - newMasterCopy - ] + ["bytes32", "address", "uint256", "address"], + [CREATE_WALLET_TYPEHASH, walletAddress, validUntil, newMasterCopy] ); const hash = eip712.hashPacked(domainSeprator, encodedRequest); diff --git a/packages/hebao_v2/test/upgrade.spec.ts b/packages/hebao_v2/test/upgrade.spec.ts index bab0fb6b8..3bb287b93 100644 --- a/packages/hebao_v2/test/upgrade.spec.ts +++ b/packages/hebao_v2/test/upgrade.spec.ts @@ -1,5 +1,8 @@ import { expect } from "./setup"; -import { signCreateWallet, signChangeMasterCopy } from "./helper/signatureUtils"; +import { + signCreateWallet, + signChangeMasterCopy +} from "./helper/signatureUtils"; import { sign } from "./helper/Signature"; import { newWallet, @@ -31,7 +34,10 @@ describe("wallet", () => { guardian1 = await account2.getAddress(); guardian2 = await account3.getAddress(); - wallet = await newWallet(owner, ethers.constants.AddressZero, 0, [guardian1, guardian2]); + wallet = await newWallet(owner, ethers.constants.AddressZero, 0, [ + guardian1, + guardian2 + ]); newSmartWalletImpl = await newWalletImpl(); }); @@ -74,8 +80,6 @@ describe("wallet", () => { // const masterCopyOfWallet = await wallet.getMasterCopy(); // console.log("masterCopyofwallet:", masterCopyOfWallet); - }); - }); }); diff --git a/packages/loopring_v3.js/src/exchange_v3.ts b/packages/loopring_v3.js/src/exchange_v3.ts index 067467469..c277153a6 100644 --- a/packages/loopring_v3.js/src/exchange_v3.ts +++ b/packages/loopring_v3.js/src/exchange_v3.ts @@ -85,9 +85,9 @@ export class ExchangeV3 { this.exchange = new web3.eth.Contract(JSON.parse(this.exchangeV3Abi)); this.exchange.options.address = this.exchangeAddress; - const exchangeCreationTimestamp = (await this.exchange.methods - .getBlockInfo(0) - .call()).timestamp; + const exchangeCreationTimestamp = ( + await this.exchange.methods.getBlockInfo(0).call() + ).timestamp; const genesisMerkleRoot = new BN( (await this.exchange.methods.getMerkleRoot().call()).slice(2), 16 @@ -626,7 +626,7 @@ export class ExchangeV3 { // Get the block data from the transaction data //const submitBlocksFunctionSignature = "0x8dadd3af"; // submitBlocks - const submitBlocksFunctionSignature = "0xdcb2aa31"; // submitBlocksWithCallbacks + const submitBlocksFunctionSignature = "0xc39ce618"; // submitBlocksWithCallbacks const transaction = await this.web3.eth.getTransaction( event.transactionHash @@ -637,6 +637,8 @@ export class ExchangeV3 { [ "bool", "bytes", + "bytes", + "bytes", "bytes" /*{ "struct CallbackConfig": { diff --git a/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol b/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol index 574b3316b..f902283dc 100644 --- a/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol +++ b/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol @@ -3,11 +3,11 @@ pragma solidity ^0.7.0; pragma experimental ABIEncoderV2; -import "../aux/access/IBlockReceiver.sol"; +import "../aux/access/ITransactionReceiver.sol"; import "../core/iface/IAgentRegistry.sol"; // import "../lib/Drainable.sol"; import "../lib/ReentrancyGuard.sol"; -import "./libamm/AmmBlockReceiver.sol"; +import "./libamm/AmmTransactionReceiver.sol"; import "./libamm/AmmData.sol"; import "./libamm/AmmExitRequest.sol"; import "./libamm/AmmJoinRequest.sol"; @@ -21,15 +21,15 @@ import "./PoolToken.sol"; contract LoopringAmmPool is PoolToken, IAgent, - IBlockReceiver, + ITransactionReceiver, ReentrancyGuard { - using AmmBlockReceiver for AmmData.State; - using AmmJoinRequest for AmmData.State; - using AmmExitRequest for AmmData.State; - using AmmPoolToken for AmmData.State; - using AmmStatus for AmmData.State; - using AmmWithdrawal for AmmData.State; + using AmmTransactionReceiver for AmmData.State; + using AmmJoinRequest for AmmData.State; + using AmmExitRequest for AmmData.State; + using AmmPoolToken for AmmData.State; + using AmmStatus for AmmData.State; + using AmmWithdrawal for AmmData.State; event PoolJoinRequested(AmmData.PoolJoin join); event PoolExitRequested(AmmData.PoolExit exit, bool force); @@ -118,7 +118,7 @@ contract LoopringAmmPool is state.exitPool(burnAmount, exitMinAmounts, true); } - function beforeBlockSubmission( + function onReceiveTransactions( bytes calldata txsData, bytes calldata callbackData ) @@ -129,7 +129,7 @@ contract LoopringAmmPool is // nonReentrant // Not needed, does not do any external calls // and can only be called by the exchange owner. { - state.beforeBlockSubmission(txsData, callbackData); + state.onReceiveTransactions(txsData, callbackData); } function withdrawWhenOffline() diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmBlockReceiver.sol b/packages/loopring_v3/contracts/amm/libamm/AmmBlockReceiver.sol deleted file mode 100644 index a0ebaf380..000000000 --- a/packages/loopring_v3/contracts/amm/libamm/AmmBlockReceiver.sol +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2017 Loopring Technology Limited. -pragma solidity ^0.7.0; -pragma experimental ABIEncoderV2; - -import "../../core/impl/libtransactions/BlockReader.sol"; -import "../../lib/MathUint.sol"; -import "./AmmData.sol"; -import "./AmmExitProcess.sol"; -import "./AmmJoinProcess.sol"; -import "./AmmPoolToken.sol"; -import "./AmmUpdateProcess.sol"; - - -/// @title AmmBlockReceiver -library AmmBlockReceiver -{ - using AmmExitProcess for AmmData.State; - using AmmJoinProcess for AmmData.State; - using AmmPoolToken for AmmData.State; - using AmmUpdateProcess for AmmData.Context; - using BlockReader for bytes; - - function beforeBlockSubmission( - AmmData.State storage S, - bytes memory txsData, - bytes calldata callbackData - ) - internal - { - AmmData.Context memory ctx = _getContext(S); - - ctx.approveAmmUpdates(txsData); - - _processPoolTx(S, ctx, txsData, callbackData); - - // Update state - S._totalSupply = ctx.totalSupply; - - // Make sure we have consumed exactly the expected number of transactions - require(txsData.length == ctx.txIdx * ExchangeData.TX_DATA_AVAILABILITY_SIZE, "INVALID_NUM_TXS"); - } - - function _getContext( - AmmData.State storage S - ) - private - view - returns (AmmData.Context memory) - { - uint size = S.tokens.length; - return AmmData.Context({ - txIdx: 0, - domainSeparator: S.domainSeparator, - accountID: S.accountID, - poolTokenID: S.poolTokenID, - feeBips: S.feeBips, - totalSupply: S._totalSupply, - tokens: S.tokens, - tokenBalancesL2: new uint96[](size) - }); - } - - function _processPoolTx( - AmmData.State storage S, - AmmData.Context memory ctx, - bytes memory txsData, - bytes calldata callbackData - ) - private - { - AmmData.PoolTx memory poolTx = abi.decode(callbackData, (AmmData.PoolTx)); - if (poolTx.txType == AmmData.PoolTxType.JOIN) { - S.processJoin( - ctx, - txsData, - abi.decode(poolTx.data, (AmmData.PoolJoin)), - poolTx.signature - ); - } else if (poolTx.txType == AmmData.PoolTxType.EXIT) { - S.processExit( - ctx, - txsData, - abi.decode(poolTx.data, (AmmData.PoolExit)), - poolTx.signature - ); - } else { - revert("INVALID_POOL_TX_TYPE"); - } - } -} diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmData.sol b/packages/loopring_v3/contracts/amm/libamm/AmmData.sol index 805c3c1e9..37e9e5a84 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmData.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmData.sol @@ -70,7 +70,8 @@ library AmmData struct Context { // functional parameters - uint txIdx; + uint txsDataPtr; + uint txsDataPtrStart; // AMM pool state variables bytes32 domainSeparator; diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol index e80e2d978..ecd4e06a4 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol @@ -33,11 +33,10 @@ library AmmExitProcess event ForcedExitProcessed(address owner, uint96 burnAmount, uint96[] amounts); function processExit( - AmmData.State storage S, - AmmData.Context memory ctx, - bytes memory txsData, - AmmData.PoolExit memory exit, - bytes memory signature + AmmData.State storage S, + AmmData.Context memory ctx, + AmmData.PoolExit memory exit, + bytes memory signature ) internal { @@ -58,14 +57,13 @@ library AmmExitProcess delete S.approvedTx[txHash]; } } else if (signature.length == 1) { - ctx.verifySignatureL2(txsData, exit.owner, txHash, signature); + ctx.verifySignatureL2(exit.owner, txHash, signature); } else { require(txHash.verifySignature(exit.owner, signature), "INVALID_OFFCHAIN_APPROVAL"); } (bool slippageOK, uint96[] memory amounts) = _calculateExitAmounts(ctx, exit); - TransferTransaction.Transfer memory transfer; if (isForcedExit) { if (!slippageOK) { AmmUtil.transferOut(address(this), exit.burnAmount, exit.owner); @@ -76,75 +74,110 @@ library AmmExitProcess ctx.totalSupply = ctx.totalSupply.sub(exit.burnAmount); } else { require(slippageOK, "EXIT_SLIPPAGE_INVALID"); - _burnPoolTokenOnL2(ctx, txsData, exit.burnAmount, exit.owner, exit.burnStorageID, signature, transfer); + _burnPoolTokenOnL2(ctx, exit.burnAmount, exit.owner, exit.burnStorageID, signature); } + _processExitTransfers( + ctx, + exit, + amounts + ); + + if (isForcedExit) { + emit ForcedExitProcessed(exit.owner, exit.burnAmount, amounts); + } + } + + function _processExitTransfers( + AmmData.Context memory ctx, + AmmData.PoolExit memory exit, + uint96[] memory amounts + ) + private + view + { // Handle liquidity tokens for (uint i = 0; i < ctx.tokens.length; i++) { - TransferTransaction.readTx(txsData, ctx.txIdx++ * ExchangeData.TX_DATA_AVAILABILITY_SIZE, transfer); + AmmData.Token memory token = ctx.tokens[i]; + // Read the transaction data + (uint packedData, address to, address from) = AmmUtil.readTransfer(ctx); + uint amount = (packedData >> 64) & 0xffffff; + uint fee = (packedData >> 32) & 0xffff; + // Decode floats + amount = (amount & 524287) * (10 ** (amount >> 19)); + fee = (fee & 2047) * (10 ** (fee >> 11)); + + uint targetAmount = uint(amounts[i]); require( - transfer.fromAccountID == ctx.accountID && + // txType == ExchangeData.TransactionType.TRANSFER && + // transfer.type == 1 && + // transfer.fromAccountID == ctx.accountID && // transfer.toAccountID == UNKNOWN && - // transfer.storageID == UNKNOWN && - transfer.from == address(this) && - transfer.to == exit.owner && - transfer.tokenID == ctx.tokens[i].tokenID && - transfer.amount.add(transfer.fee).isAlmostEqualAmount(amounts[i]), + // transfer.tokenID == token.tokenID && + packedData & 0xffffffffffff00000000ffff0000000000000000000000 == + (uint(ExchangeData.TransactionType.TRANSFER) << 176) | (1 << 168) | (uint(ctx.accountID) << 136) | (uint(token.tokenID) << 88) && + // transfer.amount.add(transfer.fee).isAlmostEqualAmount(amounts[i]) + (100000 - 8) * targetAmount <= (amount + fee) * 100000 && (amount + fee) * 100000 <= (100000 + 8) * targetAmount && + from == address(this) && + to == exit.owner, "INVALID_EXIT_TRANSFER_TX_DATA" ); - if (transfer.fee > 0) { + if (fee > 0) { require( i == ctx.tokens.length - 1 && - transfer.feeTokenID == ctx.tokens[i].tokenID && - transfer.fee <= exit.fee, + /*feeTokenID*/(packedData >> 48) & 0xffff == token.tokenID && + fee <= exit.fee, "INVALID_FEES" ); } - ctx.tokenBalancesL2[i] = ctx.tokenBalancesL2[i].sub(transfer.amount); - } - - if (isForcedExit) { - emit ForcedExitProcessed(exit.owner, exit.burnAmount, amounts); + ctx.tokenBalancesL2[i] = ctx.tokenBalancesL2[i].sub(uint96(amount)); } } function _burnPoolTokenOnL2( - AmmData.Context memory ctx, - bytes memory txsData, - uint96 amount, - address from, - uint32 burnStorageID, - bytes memory signature, - TransferTransaction.Transfer memory transfer + AmmData.Context memory ctx, + uint96 burnAmount, + address _from, + uint32 burnStorageID, + bytes memory signature ) internal view { - TransferTransaction.readTx(txsData, ctx.txIdx++ * ExchangeData.TX_DATA_AVAILABILITY_SIZE, transfer); + // Read the transaction data + (uint packedData, address to, address from) = AmmUtil.readTransfer(ctx); + uint amount = (packedData >> 64) & 0xffffff; + // Decode float + amount = (amount & 524287) * (10 ** (amount >> 19)); require( + // txType == ExchangeData.TransactionType.TRANSFER && + // transfer.type == 1 && // transfer.fromAccountID == UNKNOWN && - transfer.toAccountID == ctx.accountID && - transfer.from == from && - transfer.to == address(this) && - transfer.tokenID == ctx.poolTokenID && - transfer.amount.isAlmostEqualAmount(amount) && - transfer.feeTokenID == 0 && - transfer.fee == 0 && - (signature.length == 0 || transfer.storageID == burnStorageID), + // transfer.toAccountID == ctx.accountID && + // transfer.tokenID == ctx.poolTokenID && + // transfer.feeTokenID == 0 && + // transfer.fee == 0 && + packedData & 0xffff00000000ffffffffffff000000ffffffff00000000 == + (uint(ExchangeData.TransactionType.TRANSFER) << 176) | (1 << 168) | (uint(ctx.accountID) << 104) | (uint(ctx.poolTokenID) << 88) && + // transfer.amount.isAlmostEqualAmount(burnAmount) && + (100000 - 8) * burnAmount <= amount * 100000 && amount * 100000 <= (100000 + 8) * burnAmount && + to == address(this) && + from == _from && + (signature.length == 0 || /*storageID*/(packedData & 0xffffffff) == burnStorageID), "INVALID_BURN_TX_DATA" ); // Update pool balance - ctx.totalSupply = ctx.totalSupply.sub(transfer.amount); + ctx.totalSupply = ctx.totalSupply.sub(uint96(amount)); } function _calculateExitAmounts( - AmmData.Context memory ctx, - AmmData.PoolExit memory exit + AmmData.Context memory ctx, + AmmData.PoolExit memory exit ) private pure @@ -159,10 +192,11 @@ library AmmExitProcess uint ratio = uint(AmmData.POOL_TOKEN_BASE).mul(exit.burnAmount) / ctx.totalSupply; for (uint i = 0; i < ctx.tokens.length; i++) { - amounts[i] = (ratio.mul(ctx.tokenBalancesL2[i]) / AmmData.POOL_TOKEN_BASE).toUint96(); - if (amounts[i] < exit.exitMinAmounts[i]) { + uint96 amount = (ratio.mul(ctx.tokenBalancesL2[i]) / AmmData.POOL_TOKEN_BASE).toUint96(); + if (amount < exit.exitMinAmounts[i]) { return (false, amounts); } + amounts[i] = amount; } return (true, amounts); diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmExitRequest.sol b/packages/loopring_v3/contracts/amm/libamm/AmmExitRequest.sol index c4af2d3b4..7510bafea 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmExitRequest.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmExitRequest.sol @@ -65,9 +65,9 @@ library AmmExitRequest ) internal pure - returns (bytes32) + returns (bytes32 h) { - return EIP712.hashPacked( + /*return EIP712.hashPacked( domainSeparator, keccak256( abi.encode( @@ -80,6 +80,28 @@ library AmmExitRequest exit.validUntil ) ) - ); + );*/ + bytes32 typeHash = POOLEXIT_TYPEHASH; + address owner = exit.owner; + uint burnAmount = exit.burnAmount; + uint burnStorageID = exit.burnStorageID; + uint96[] memory exitMinAmounts = exit.exitMinAmounts; + uint fee = exit.fee; + uint validUntil = exit.validUntil; + assembly { + let data := mload(0x40) + mstore( data , typeHash) + mstore(add(data, 32), owner) + mstore(add(data, 64), burnAmount) + mstore(add(data, 96), burnStorageID) + mstore(add(data, 128), keccak256(add(exitMinAmounts, 32), mul(mload(exitMinAmounts), 32))) + mstore(add(data, 160), fee) + mstore(add(data, 192), validUntil) + let p := keccak256(data, 224) + mstore(data, "\x19\x01") + mstore(add(data, 2), domainSeparator) + mstore(add(data, 34), p) + h := keccak256(data, 66) + } } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol index 4ec0caa6b..fc8a53fdb 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol @@ -31,23 +31,21 @@ library AmmJoinProcess // event JoinProcessed(address owner, uint96 mintAmount, uint96[] amounts); function processJoin( - AmmData.State storage S, - AmmData.Context memory ctx, - bytes memory txsData, - AmmData.PoolJoin memory join, - bytes memory signature + AmmData.State storage S, + AmmData.Context memory ctx, + AmmData.PoolJoin memory join, + bytes memory signature ) internal { require(join.validUntil >= block.timestamp, "EXPIRED"); bytes32 txHash = AmmJoinRequest.hash(ctx.domainSeparator, join); - if (signature.length == 0) { require(S.approvedTx[txHash], "INVALID_ONCHAIN_APPROVAL"); delete S.approvedTx[txHash]; } else if (signature.length == 1) { - ctx.verifySignatureL2(txsData, join.owner, txHash, signature); + ctx.verifySignatureL2(join.owner, txHash, signature); } else { require(txHash.verifySignature(join.owner, signature), "INVALID_OFFCHAIN_L1_APPROVAL"); } @@ -56,71 +54,111 @@ library AmmJoinProcess (bool slippageOK, uint96 mintAmount, uint96[] memory amounts) = _calculateJoinAmounts(ctx, join); require(slippageOK, "JOIN_SLIPPAGE_INVALID"); + // Process transfers + _processJoinTransfers( + ctx, + join, + amounts, + signature + ); + + _mintPoolTokenOnL2( + ctx, + mintAmount, + join.owner + ); + + // emit JoinProcessed(join.owner, mintAmount, amounts); + } + + function _processJoinTransfers( + AmmData.Context memory ctx, + AmmData.PoolJoin memory join, + uint96[] memory amounts, + bytes memory signature + ) + private + view + { // Handle liquidity tokens - TransferTransaction.Transfer memory transfer; for (uint i = 0; i < ctx.tokens.length; i++) { - TransferTransaction.readTx(txsData, ctx.txIdx++ * ExchangeData.TX_DATA_AVAILABILITY_SIZE, transfer); + AmmData.Token memory token = ctx.tokens[i]; + + // Read the transaction data + (uint packedData, address to, address from) = AmmUtil.readTransfer(ctx); + uint amount = (packedData >> 64) & 0xffffff; + uint fee = (packedData >> 32) & 0xffff; + + // Decode float + amount = (amount & 524287) * (10 ** (amount >> 19)); + uint targetAmount = uint(amounts[i]); require( + // txType == ExchangeData.TransactionType.TRANSFER && + // transfer.type == 1 && // transfer.fromAccountID == UNKNOWN && - transfer.toAccountID == ctx.accountID && - transfer.from == join.owner && - transfer.to == address(this) && - transfer.tokenID == ctx.tokens[i].tokenID && - transfer.amount.isAlmostEqualAmount(amounts[i]) && - (signature.length == 0 || transfer.storageID == join.joinStorageIDs[i]), + // transfer.toAccountID == ctx.accountID && + // transfer.tokenID == token.tokenID && + packedData & 0xffff00000000ffffffffffff0000000000000000000000 == + (uint(ExchangeData.TransactionType.TRANSFER) << 176) | (1 << 168) | (uint(ctx.accountID) << 104) | (uint(token.tokenID) << 88) && + (100000 - 8) * targetAmount <= amount * 100000 && amount * 100000 <= (100000 + 8) * targetAmount && + (signature.length == 0 || /*storageID*/(packedData & 0xffffffff) == join.joinStorageIDs[i]) && + from == join.owner && + to == address(this), "INVALID_JOIN_TRANSFER_TX_DATA" ); - if (transfer.fee > 0) { + if (fee > 0) { + // Decode float + fee = (fee & 2047) * (10 ** (fee >> 11)); require( i == ctx.tokens.length - 1 && - transfer.feeTokenID == ctx.tokens[i].tokenID && - transfer.fee <= join.fee, + /*feeTokenID*/(packedData >> 48) & 0xffff == token.tokenID && + fee <= join.fee, "INVALID_FEES" ); } - ctx.tokenBalancesL2[i] = ctx.tokenBalancesL2[i].add(transfer.amount); + ctx.tokenBalancesL2[i] = ctx.tokenBalancesL2[i].add(uint96(amount)); } - - _mintPoolTokenOnL2(ctx, txsData, mintAmount, join.owner, transfer); - - // emit JoinProcessed(join.owner, mintAmount, amounts); } function _mintPoolTokenOnL2( - AmmData.Context memory ctx, - bytes memory txsData, - uint96 amount, - address to, - TransferTransaction.Transfer memory transfer + AmmData.Context memory ctx, + uint mintAmount, + address _to ) private view { - TransferTransaction.readTx(txsData, ctx.txIdx++ * ExchangeData.TX_DATA_AVAILABILITY_SIZE, transfer); + // Read the transaction data + (uint packedData, address to, address from) = AmmUtil.readTransfer(ctx); + uint amount = (packedData >> 64) & 0xffffff; + // Decode float + amount = (amount & 524287) * (10 ** (amount >> 19)); require( - transfer.fromAccountID == ctx.accountID && + // txType == ExchangeData.TransactionType.TRANSFER && + // transfer.type == 1 && + // transfer.fromAccountID == ctx.accountID && // transfer.toAccountID == UNKNOWN && - transfer.from == address(this) && - transfer.to == to && - transfer.tokenID == ctx.poolTokenID && - transfer.amount.isAlmostEqualAmount(amount) && - transfer.feeTokenID == 0 && - transfer.fee == 0, - // transfer.storageID == UNKNOWN && + // transfer.tokenID == ctx.poolTokenID && + packedData & 0xffffffffffff00000000ffff000000ffffffff00000000 == + (uint(ExchangeData.TransactionType.TRANSFER) << 176) | (1 << 168) | (uint(ctx.accountID) << 136) | (uint(ctx.poolTokenID) << 88) && + // transfer.amount.isAlmostEqualAmount(mintAmount) && + (100000 - 8) * mintAmount <= amount * 100000 && amount * 100000 <= (100000 + 8) * mintAmount && + to == _to && + from == address(this), "INVALID_MINT_TX_DATA" ); // Update pool balance - ctx.totalSupply = ctx.totalSupply.add(transfer.amount); + ctx.totalSupply = ctx.totalSupply.add(uint96(amount)); } function _calculateJoinAmounts( - AmmData.Context memory ctx, - AmmData.PoolJoin memory join + AmmData.Context memory ctx, + AmmData.PoolJoin memory join ) private pure diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmJoinRequest.sol b/packages/loopring_v3/contracts/amm/libamm/AmmJoinRequest.sol index b322580a1..1ca9733b6 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmJoinRequest.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmJoinRequest.sol @@ -52,9 +52,9 @@ library AmmJoinRequest ) internal pure - returns (bytes32) + returns (bytes32 h) { - return EIP712.hashPacked( + /*return EIP712.hashPacked( domainSeparator, keccak256( abi.encode( @@ -67,6 +67,28 @@ library AmmJoinRequest join.validUntil ) ) - ); + );*/ + bytes32 typeHash = POOLJOIN_TYPEHASH; + address owner = join.owner; + uint96[] memory joinAmounts = join.joinAmounts; + uint32[] memory storageIDs = join.joinStorageIDs; + uint mintMinAmount = join.mintMinAmount; + uint fee = join.fee; + uint validUntil = join.validUntil; + assembly { + let data := mload(0x40) + mstore( data , typeHash) + mstore(add(data, 32), owner) + mstore(add(data, 64), keccak256(add(joinAmounts, 32), mul(mload(joinAmounts), 32))) + mstore(add(data, 96), keccak256(add(storageIDs, 32), mul(mload(storageIDs), 32))) + mstore(add(data, 128), mintMinAmount) + mstore(add(data, 160), fee) + mstore(add(data, 192), validUntil) + let p := keccak256(data, 224) + mstore(data, "\x19\x01") + mstore(add(data, 2), domainSeparator) + mstore(add(data, 34), p) + h := keccak256(data, 66) + } } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmTransactionReceiver.sol b/packages/loopring_v3/contracts/amm/libamm/AmmTransactionReceiver.sol new file mode 100644 index 000000000..f0a89a664 --- /dev/null +++ b/packages/loopring_v3/contracts/amm/libamm/AmmTransactionReceiver.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../../core/impl/libtransactions/BlockReader.sol"; +import "../../lib/MathUint.sol"; +import "./AmmData.sol"; +import "./AmmExitProcess.sol"; +import "./AmmJoinProcess.sol"; +import "./AmmPoolToken.sol"; +import "./AmmUpdateProcess.sol"; + + +/// @title AmmTransactionReceiver +library AmmTransactionReceiver +{ + using AmmExitProcess for AmmData.State; + using AmmJoinProcess for AmmData.State; + using AmmPoolToken for AmmData.State; + using AmmUpdateProcess for AmmData.Context; + using BlockReader for bytes; + + function onReceiveTransactions( + AmmData.State storage S, + bytes calldata txsData, + bytes calldata callbackData + ) + internal + { + AmmData.Context memory ctx = _getContext(S, txsData); + + ctx.approveAmmUpdates(); + + _processPoolTx(S, ctx, callbackData); + + // Update state + S._totalSupply = ctx.totalSupply; + + // Make sure we have consumed exactly the expected number of transactions + require(txsData.length == ctx.txsDataPtr - ctx.txsDataPtrStart, "INVALID_NUM_TXS"); + } + + function _getContext( + AmmData.State storage S, + bytes calldata txsData + ) + private + view + returns (AmmData.Context memory) + { + uint size = S.tokens.length; + uint txsDataPtr = 23; + assembly { + txsDataPtr := sub(add(txsData.offset, txsDataPtr), 32) + } + return AmmData.Context({ + txsDataPtr: txsDataPtr, + txsDataPtrStart: txsDataPtr, + domainSeparator: S.domainSeparator, + accountID: S.accountID, + poolTokenID: S.poolTokenID, + feeBips: S.feeBips, + totalSupply: S._totalSupply, + tokens: S.tokens, + tokenBalancesL2: new uint96[](size) + }); + } + + function _processPoolTx( + AmmData.State storage S, + AmmData.Context memory ctx, + bytes calldata callbackData + ) + private + { + // Manually decode the encoded PoolTx in `callbackData` + AmmData.PoolTxType txType; + bytes calldata data; + bytes calldata signature; + assembly { + txType := calldataload(add(callbackData.offset, 0x20)) + + data.offset := add(add(callbackData.offset, 0x20), calldataload(add(callbackData.offset, 0x40))) + data.length := calldataload(data.offset) + data.offset := add(data.offset, 0x20) + + signature.offset := add(add(callbackData.offset, 0x20), calldataload(add(callbackData.offset, 0x60))) + signature.length := calldataload(signature.offset) + signature.offset := add(signature.offset, 0x20) + } + if (txType == AmmData.PoolTxType.JOIN) { + S.processJoin( + ctx, + abi.decode(data, (AmmData.PoolJoin)), + signature + ); + } else if (txType == AmmData.PoolTxType.EXIT) { + S.processExit( + ctx, + abi.decode(data, (AmmData.PoolExit)), + signature + ); + } else { + revert("INVALID_POOL_TX_TYPE"); + } + } +} diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol index 0c62d0869..f00dc34a4 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol @@ -15,27 +15,36 @@ library AmmUpdateProcess using TransactionReader for ExchangeData.Block; function approveAmmUpdates( - AmmData.Context memory ctx, - bytes memory txsData + AmmData.Context memory ctx ) internal view { - AmmUpdateTransaction.AmmUpdate memory update; + uint txsDataPtr = ctx.txsDataPtr + 5; for (uint i = 0; i < ctx.tokens.length; i++) { - // Check that the AMM update in the block matches the expected update - AmmUpdateTransaction.readTx(txsData, ctx.txIdx++ * ExchangeData.TX_DATA_AVAILABILITY_SIZE, update); + // txType | owner | accountID | tokenID | feeBips + uint packedDataA; + // tokenWeight | nonce | balance + uint packedDataB; + assembly { + packedDataA := calldataload(txsDataPtr) + packedDataB := calldataload(add(txsDataPtr, 28)) + } + + AmmData.Token memory token = ctx.tokens[i]; require( - update.owner == address(this) && - update.accountID == ctx.accountID && - update.tokenID == ctx.tokens[i].tokenID && - update.feeBips == ctx.feeBips && - update.tokenWeight == ctx.tokens[i].weight, + packedDataA & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff == + (uint(ExchangeData.TransactionType.AMM_UPDATE) << 216) | (uint(address(this)) << 56) | (ctx.accountID << 24) | (token.tokenID << 8) | ctx.feeBips && + (packedDataB >> 128) & 0xffffffffffffffffffffffff == token.weight, "INVALID_AMM_UPDATE_TX_DATA" ); - ctx.tokenBalancesL2[i] = update.balance; + ctx.tokenBalancesL2[i] = uint96(packedDataB & 0xffffffffffffffffffffffff); + + txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; } + + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE * ctx.tokens.length; } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmUtil.sol b/packages/loopring_v3/contracts/amm/libamm/AmmUtil.sol index d274e9c41..1fdab5adc 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmUtil.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmUtil.sol @@ -24,11 +24,10 @@ library AmmUtil uint8 public constant L2_SIGNATURE_TYPE = 16; function verifySignatureL2( - AmmData.Context memory ctx, - bytes memory txsData, - address owner, - bytes32 txHash, - bytes memory signature + AmmData.Context memory ctx, + address owner, + bytes32 txHash, + bytes memory signature ) internal pure @@ -37,15 +36,38 @@ library AmmUtil require(signature.toUint8Unsafe(0) == L2_SIGNATURE_TYPE, "INVALID_SIGNATURE_TYPE"); // Read the signature verification transaction - SignatureVerificationTransaction.SignatureVerification memory verification; - SignatureVerificationTransaction.readTx(txsData, ctx.txIdx++ * ExchangeData.TX_DATA_AVAILABILITY_SIZE, verification); + uint txsDataPtr = ctx.txsDataPtr - 2; + uint packedData; + uint data; + assembly { + packedData := calldataload(txsDataPtr) + data := calldataload(add(txsDataPtr, 36)) + } // Verify that the hash was signed on L2 require( - verification.owner == owner && - verification.data == uint(txHash) >> 3, + packedData & 0xffffffffffffffffffffffffffffffffffffffffff == + (uint(ExchangeData.TransactionType.SIGNATURE_VERIFICATION) << 160) | (uint(owner) & 0x00ffffffffffffffffffffffffffffffffffffffff) && + data == uint(txHash) >> 3, "INVALID_OFFCHAIN_L2_APPROVAL" ); + + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; + } + + function readTransfer(AmmData.Context memory ctx) + internal + pure + returns (uint packedData, address to, address from) + { + uint txsDataPtr = ctx.txsDataPtr; + // packedData: txType (1) | type (1) | fromAccountID (4) | toAccountID (4) | tokenID (2) | amount (3) | feeTokenID (2) | fee (2) | storageID (4) + assembly { + packedData := calldataload(txsDataPtr) + to := and(calldataload(add(txsDataPtr, 20)), 0xffffffffffffffffffffffffffffffffffffffff) + from := and(calldataload(add(txsDataPtr, 40)), 0xffffffffffffffffffffffffffffffffffffffff) + } + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; } function isAlmostEqualAmount( @@ -56,15 +78,12 @@ library AmmUtil pure returns (bool) { - if (targetAmount == 0) { - return amount == 0; - } else { - // Max rounding error for a float24 is 2/100000 - // But relayer may use float rounding multiple times - // so the range is expanded to [100000 - 8, 100000 + 8] - uint ratio = (uint(amount) * 100000) / uint(targetAmount); - return (100000 - 8) <= ratio && ratio <= (100000 + 8); - } + uint _amount = uint(amount) * 100000; + uint _targetAmount = uint(targetAmount); + // Max rounding error for a float24 is 2/100000 + // But relayer may use float rounding multiple times + // so the range is expanded to [100000 - 8, 100000 + 8] + return (100000 - 8) * _targetAmount <= _amount && _amount <= (100000 + 8) * _targetAmount; } function isAlmostEqualFee( diff --git a/packages/loopring_v3/contracts/aux/access/IBlockReceiver.sol b/packages/loopring_v3/contracts/aux/access/ITransactionReceiver.sol similarity index 63% rename from packages/loopring_v3/contracts/aux/access/IBlockReceiver.sol rename to packages/loopring_v3/contracts/aux/access/ITransactionReceiver.sol index f8d133330..1f740ed9c 100644 --- a/packages/loopring_v3/contracts/aux/access/IBlockReceiver.sol +++ b/packages/loopring_v3/contracts/aux/access/ITransactionReceiver.sol @@ -6,13 +6,13 @@ pragma experimental ABIEncoderV2; import "../../core/iface/ExchangeData.sol"; import "../../amm/libamm/AmmData.sol"; -/// @title IBlockReceiver +/// @title ITransactionReceiver /// @author Brecht Devos - -abstract contract IBlockReceiver +abstract contract ITransactionReceiver { - function beforeBlockSubmission( - bytes calldata txsData, - bytes calldata callbackData + function onReceiveTransactions( + bytes calldata txsData, + bytes calldata callbackData ) external virtual; diff --git a/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol b/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol index 56b6dd983..37464edf9 100644 --- a/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol +++ b/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol @@ -13,7 +13,7 @@ import "../../lib/ERC1271.sol"; import "../../lib/MathUint.sol"; import "../../lib/SignatureUtil.sol"; import "./SelectorBasedAccessManager.sol"; -import "./IBlockReceiver.sol"; +import "./ITransactionReceiver.sol"; contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainable @@ -38,16 +38,23 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab bytes data; } - struct BlockCallback + struct TransactionReceiverCallback { - uint16 blockIdx; - TxCallback[] txCallbacks; + uint16 blockIdx; + TxCallback[] txCallbacks; } - struct CallbackConfig + struct TransactionReceiverCallbacks { - BlockCallback[] blockCallbacks; - address[] receivers; + TransactionReceiverCallback[] callbacks; + address[] receivers; + } + + struct Callback + { + address to; + bytes data; + bool before; } constructor( @@ -91,13 +98,15 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab } function submitBlocksWithCallbacks( - bool isDataCompressed, - bytes calldata data, - CallbackConfig calldata config + bool isDataCompressed, + bytes calldata data, + TransactionReceiverCallbacks calldata config, + ExchangeData.FlashMint[] calldata flashMints, + Callback[] calldata callbacks ) external { - if (config.blockCallbacks.length > 0) { + if (config.callbacks.length > 0) { require(config.receivers.length > 0, "MISSING_RECEIVERS"); // Make sure the receiver is authorized to approve transactions @@ -123,15 +132,32 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab // Decode the blocks ExchangeData.Block[] memory blocks = _decodeBlocks(decompressed); - // Process the callback logic. - _beforeBlockSubmission(blocks, config); + // Do pre blocks callbacks + _processCallbacks(callbacks, true); + + // Do flash mints + if (flashMints.length > 0) { + IExchangeV3(target).flashMint(flashMints); + } + // Submit blocks target.fastCallAndVerify(gasleft(), 0, decompressed); + + // Do transaction verifying blocks callbacks + _verifyTransactions(blocks, config); + + // Do post blocks callbacks + _processCallbacks(callbacks, false); + + // Make sure flash mints were repaid + if (flashMints.length > 0) { + IExchangeV3(target).verifyFlashMintsPaidBack(flashMints); + } } - function _beforeBlockSubmission( - ExchangeData.Block[] memory blocks, - CallbackConfig calldata config + function _verifyTransactions( + ExchangeData.Block[] memory blocks, + TransactionReceiverCallbacks calldata config ) private { @@ -143,10 +169,10 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab // Process transactions int lastBlockIdx = -1; - for (uint i = 0; i < config.blockCallbacks.length; i++) { - BlockCallback calldata blockCallback = config.blockCallbacks[i]; + for (uint i = 0; i < config.callbacks.length; i++) { + TransactionReceiverCallback calldata callback = config.callbacks[i]; - uint16 blockIdx = blockCallback.blockIdx; + uint16 blockIdx = callback.blockIdx; require(blockIdx > lastBlockIdx, "BLOCK_INDEX_OUT_OF_ORDER"); lastBlockIdx = int(blockIdx); @@ -155,7 +181,7 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab _processTxCallbacks( _block, - blockCallback.txCallbacks, + callback.txCallbacks, config.receivers, preApprovedTxs[blockIdx] ); @@ -170,13 +196,14 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab auxiliaryData := add(blockAuxData, 64) } + uint txIdx; + bool approved; + uint auxOffset; for(uint j = 0; j < auxiliaryData.length; j++) { // Load the data from auxiliaryData, which is still encoded as calldata - uint txIdx; - bool approved; assembly { // Offset to auxiliaryData[j] - let auxOffset := mload(add(auxiliaryData, add(32, mul(32, j)))) + auxOffset := mload(add(auxiliaryData, add(32, mul(32, j)))) // Load `txIdx` (pos 0) and `approved` (pos 1) in auxiliaryData[j] txIdx := mload(add(add(32, auxiliaryData), auxOffset)) approved := mload(add(add(64, auxiliaryData), auxOffset)) @@ -187,8 +214,34 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab } } + function _processCallbacks( + Callback[] calldata callbacks, + bool before + ) + private + { + for (uint i = 0; i < callbacks.length; i++) { + Callback calldata callback = callbacks[i]; + if (callback.before != before) { + continue; + } + + // Disallow calls to self, the exchange and TransactionReceiver functions + require( + callback.to != target && + callback.to != address(this), + "EXCHANGE_CANNOT_BE_POST_CALLBACK_TARGET" + ); + require(callback.data.toBytes4(0) != ITransactionReceiver.onReceiveTransactions.selector, "INVALID_POST_CALLBACK_FUNCTION"); + (bool success, bytes memory returnData) = callback.to.call(callback.data); + if (!success) { + assembly { revert(add(returnData, 32), mload(returnData)) } + } + } + } + function _processTxCallbacks( - ExchangeData.Block memory _block, + ExchangeData.Block memory _block, TxCallback[] calldata txCallbacks, address[] calldata receivers, bool[] memory preApprovedTxs @@ -201,25 +254,15 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab uint cursor = 0; - // Reuse the data when possible to save on some memory alloc gas - bytes memory txsData = new bytes(txCallbacks[0].numTxs * ExchangeData.TX_DATA_AVAILABILITY_SIZE); for (uint i = 0; i < txCallbacks.length; i++) { TxCallback calldata txCallback = txCallbacks[i]; + require(txCallback.receiverIdx < receivers.length, "INVALID_RECEIVER_INDEX"); uint txIdx = uint(txCallback.txIdx); require(txIdx >= cursor, "TX_INDEX_OUT_OF_ORDER"); - require(txCallback.receiverIdx < receivers.length, "INVALID_RECEIVER_INDEX"); - - uint txsDataLength = ExchangeData.TX_DATA_AVAILABILITY_SIZE*txCallback.numTxs; - if (txsData.length != txsDataLength) { - txsData = new bytes(txsDataLength); - } - _block.readTxs(txIdx, txCallback.numTxs, txsData); - IBlockReceiver(receivers[txCallback.receiverIdx]).beforeBlockSubmission( - txsData, - txCallback.data - ); + // Execute callback + _callCallback(_block, txCallback, receivers[txCallback.receiverIdx]); // Now that the transactions have been verified, mark them as approved for (uint j = txIdx; j < txIdx + txCallback.numTxs; j++) { @@ -230,6 +273,52 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab } } + function _callCallback( + ExchangeData.Block memory _block, + TxCallback calldata txCallback, + address receiver + ) + private + { + bytes memory txData; + bytes memory txsData; + + // Construct the calldata passed into the callback call + bytes calldata callbackData = txCallback.data; + bytes4 selector = ITransactionReceiver.onReceiveTransactions.selector; + + uint txsDataLength = ExchangeData.TX_DATA_AVAILABILITY_SIZE*txCallback.numTxs; + uint callbackDataLength = txCallback.data.length; + // Bytes arrays are always padded with zeros so they are aligned to 32 bytes + uint newCallbackDataOffset = 32 + 32 + 32 + ((txsDataLength + 31) / 32 * 32); + uint totalLength = 32 + newCallbackDataOffset + 32 + ((callbackDataLength + 31) / 32 * 32); + assembly { + txData := mload(0x40) + mstore(txData, totalLength) + mstore(add(txData, 32), selector) + + // Offset to txsData + mstore(add(txData, 36), 0x40) + // Offset to callbackData + mstore(add(txData, 68), newCallbackDataOffset) + + // txsData + txsData := add(txData, 100) + mstore(txsData, txsDataLength) + + // copy callbackData + calldatacopy(add(txData, add(36, newCallbackDataOffset)), sub(callbackData.offset, 32), add(callbackDataLength, 32)) + + mstore(0x40, add(add(txData, totalLength), 32)) + } + + // Copy the necessary block transaction data directly to the correct place in the calldata + _block.readTxs(uint(txCallback.txIdx), txCallback.numTxs, txsData); + + // Do the actual call with the constructed calldata + receiver.fastCallAndVerify(gasleft(), 0, txData); + } + function _decodeBlocks(bytes memory data) private pure diff --git a/packages/loopring_v3/contracts/aux/bridge/Bridge.sol b/packages/loopring_v3/contracts/aux/bridge/Bridge.sol new file mode 100644 index 000000000..b23c1ab38 --- /dev/null +++ b/packages/loopring_v3/contracts/aux/bridge/Bridge.sol @@ -0,0 +1,891 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../../core/iface/IExchangeV3.sol"; +import "../../core/impl/libtransactions/TransferTransaction.sol"; +import "../../core/impl/libtransactions/WithdrawTransaction.sol"; +import "../../lib/AddressUtil.sol"; +import "../../lib/ERC20SafeTransfer.sol"; +import "../../lib/ERC20.sol"; +import "../../lib/MathUint.sol"; +import "../../lib/MathUint96.sol"; +import "../../lib/ReentrancyGuard.sol"; + +import "./IBridge.sol"; + + +/// @title Bridge implementation +/// @author Brecht Devos - +contract Bridge is IBridge, ReentrancyGuard, Claimable +{ + using AddressUtil for address; + using AddressUtil for address payable; + using BytesUtil for bytes; + using ERC20SafeTransfer for address; + using MathUint for uint; + using MathUint96 for uint96; + + // Transfers packed as: + // - address owner : 20 bytes + // - uint96 amount : 12 bytes + // - uint16 tokenID: 2 bytes + event Transfers (uint batchID, bytes transfers, address from); + + event ConnectorCallResult (address connector, bool success, bytes reason); + + event ConnectorTrusted (address connector, bool trusted); + + struct InternalBridgeTransfer + { + address owner; + uint16 tokenID; + uint96 amount; + } + + struct TokenData + { + address token; + uint16 tokenID; + uint amount; + } + + struct ConnectorCalls + { + address connector; + uint gasLimit; + ConnectorGroup[] groups; + } + + struct TransferBatch + { + uint batchID; + uint96[] amounts; + } + + struct BridgeOperations + { + TransferBatch[] transferBatches; + ConnectorCalls[] connectorCalls; + TokenData[] tokens; + } + + struct Context + { + TokenData[] tokens; + uint tokensOffset; + uint txsDataPtr; + uint txsDataPtrStart; + } + + struct CallTransfer + { + uint fromAccountID; + uint tokenID; + uint amount; + uint feeTokenID; + uint fee; + uint storageID; + uint packedData; + } + + bytes32 constant public BRIDGE_CALL_TYPEHASH = keccak256( + "BridgeCall(uint16 tokenID,uint96 amount,uint16 feeTokenID,uint96 maxFee,uint32 validUntil,uint32 storageID,uint32 minGas,address connector,bytes groupData,bytes userData)" + ); + + uint public constant MAX_NUM_TRANSACTIONS_IN_BLOCK = 386; + uint public constant MAX_AGE_PENDING_TRANSFER = 7 days; + uint public constant MAX_FEE_BIPS = 25; // 0.25% + uint public constant GAS_LIMIT_CHECK_GAS_LIMIT = 10000; + + IExchangeV3 public immutable exchange; + uint32 public immutable accountID; + IDepositContract public immutable depositContract; + bytes32 public immutable DOMAIN_SEPARATOR; + + address public exchangeOwner; + + mapping (uint => mapping (bytes32 => uint)) public pendingTransfers; + mapping (uint => mapping(uint => bool)) public withdrawn; + + mapping (address => bool) public trustedConnectors; + + uint public batchIDGenerator; + + // token -> tokenID + mapping (address => uint16) public cachedTokenIDs; + + modifier onlyFromExchangeOwner() + { + require(msg.sender == exchangeOwner, "UNAUTHORIZED"); + _; + } + + constructor( + IExchangeV3 _exchange, + uint32 _accountID + ) + { + exchange = _exchange; + accountID = _accountID; + + depositContract = _exchange.getDepositContract(); + exchangeOwner = _exchange.owner(); + + DOMAIN_SEPARATOR = EIP712.hash(EIP712.Domain("Bridge", "1.0", address(this))); + } + + function batchDeposit( + BridgeTransfer[] memory deposits + ) + public + payable + override + { + BridgeTransfer[][] memory _deposits = new BridgeTransfer[][](1); + _deposits[0] = deposits; + _batchDeposit(msg.sender,_deposits); + } + + function onReceiveTransactions( + bytes calldata txsData, + bytes calldata /*callbackData*/ + ) + external + onlyFromExchangeOwner + { + uint txsDataPtr = 23; + assembly { + txsDataPtr := sub(add(txsData.offset, txsDataPtr), 32) + } + Context memory ctx = Context({ + tokens: new TokenData[](0), + tokensOffset: 0, + txsDataPtr: txsDataPtr, + txsDataPtrStart: txsDataPtr + }); + + _processTransactions(ctx); + + // Make sure we have consumed exactly the expected number of transactions + require(txsData.length == ctx.txsDataPtr - ctx.txsDataPtrStart, "INVALID_NUM_TXS"); + } + + // Allows withdrawing from pending transfers that are at least MAX_AGE_PENDING_TRANSFER old. + function withdrawFromPendingBatchDeposit( + uint batchID, + InternalBridgeTransfer[] memory transfers, + uint[] memory indices + ) + external + nonReentrant + { + bytes memory transfersData = new bytes(transfers.length * 34); + assembly { + transfersData := add(transfersData, 32) + } + + for (uint i = 0; i < transfers.length; i++) { + InternalBridgeTransfer memory transfer = transfers[i]; + // Pack the transfer data to compare agains batch deposit hash + address owner = transfer.owner; + uint16 tokenID = transfer.tokenID; + uint amount = transfer.amount; + assembly { + mstore(add(transfersData, 2), tokenID) + mstore( transfersData , or(shl(96, owner), amount)) + transfersData := add(transfersData, 34) + } + } + + // Get the original transfers ptr back + uint numTransfers = transfers.length; + assembly { + transfersData := sub(transfersData, add(32, mul(34, numTransfers))) + } + + // Check if withdrawing from these transfers is possible + bytes32 hash = _hashTransferBatch(transfersData); + require(_arePendingTransfersTooOld(batchID, hash), "TRANSFERS_NOT_TOO_OLD"); + + for (uint i = 0; i < indices.length; i++) { + uint idx = indices[i]; + + require(!withdrawn[batchID][idx], "ALREADY_WITHDRAWN"); + withdrawn[batchID][idx] = true; + + address tokenAddress = exchange.getTokenAddress(transfers[idx].tokenID); + + _transferOut( + tokenAddress, + transfers[idx].amount, + transfers[idx].owner + ); + } + } + + // Can be used to withdraw funds that were already deposited to the bridge, + // but need to be returned to be able to withdraw from old pending transfers. + function forceWithdraw(address[] calldata tokens) + external + payable + nonReentrant + { + for (uint i = 0; i < tokens.length; i++) { + exchange.forceWithdraw{value: msg.value / tokens.length}( + address(this), + tokens[i], + accountID + ); + } + } + + function setConnectorTrusted( + address connector, + bool trusted + ) + external + onlyOwner + { + trustedConnectors[connector] = trusted; + emit ConnectorTrusted(connector, trusted); + } + + receive() + external + payable + {} + + // --- Internal functions --- + + function _batchDeposit( + address from, + BridgeTransfer[][] memory deposits + ) + internal + { + uint totalNumDeposits = 0; + for (uint i = 0; i < deposits.length; i++) { + totalNumDeposits += deposits[i].length; + } + if (totalNumDeposits == 0) { + return; + } + + // Needs to be possible to do all transfers in a single block + require(totalNumDeposits <= MAX_NUM_TRANSACTIONS_IN_BLOCK, "MAX_DEPOSITS_EXCEEDED"); + + // Transfers to be done + bytes memory transfers = new bytes(totalNumDeposits * 34); + assembly { + transfers := add(transfers, 32) + } + + // Worst case scenario all tokens are different + TokenData[] memory tokens = new TokenData[](totalNumDeposits); + uint numDistinctTokens = 0; + + // Run over all deposits summing up total amounts per token + address token = address(-1); + uint tokenIdx = 0; + uint16 tokenID; + BridgeTransfer memory deposit; + for (uint n = 0; n < deposits.length; n++) { + BridgeTransfer[] memory _deposits = deposits[n]; + for (uint i = 0; i < _deposits.length; i++) { + deposit = _deposits[i]; + if(token != deposit.token) { + token = deposit.token; + tokenIdx = 0; + while(tokenIdx < numDistinctTokens && tokens[tokenIdx].token != token) { + tokenIdx++; + } + if (tokenIdx == numDistinctTokens) { + tokens[tokenIdx].token = token; + tokens[tokenIdx].tokenID = _getTokenID(token); + numDistinctTokens++; + } + tokenID = tokens[tokenIdx].tokenID; + } + tokens[tokenIdx].amount = tokens[tokenIdx].amount.add(deposit.amount); + + // Pack the transfer data together + assembly { + mstore(add(transfers, 2), tokenID) + mstore( transfers , or(shl(96, mload(deposit)), mload(add(deposit, 64)))) + transfers := add(transfers, 34) + } + } + } + + // Get the original transfers ptr back + assembly { + transfers := sub(transfers, add(32, mul(34, totalNumDeposits))) + } + + // Do a normal deposit per token + for(uint i = 0; i < numDistinctTokens; i++) { + if (tokens[i].token == address(0)) { + require(tokens[i].amount == msg.value || from == address(this), "INVALID_ETH_DEPOSIT"); + } + _deposit(from, tokens[i].token, uint96(tokens[i].amount)); + } + + // Store the transfers so they can be processed later + _storeTransfers(transfers, from); + } + + function _processTransactions(Context memory ctx) + internal + { + // Get the calldata structs directly from the encoded calldata bytes data + TransferBatch[] calldata transferBatches; + ConnectorCalls[] calldata connectorCalls; + TokenData[] calldata tokens; + uint tokensOffset; + assembly { + let offsetToCallbackData := add(68, calldataload(36)) + // transferBatches + transferBatches.offset := add(add(offsetToCallbackData, 32), calldataload(offsetToCallbackData)) + transferBatches.length := calldataload(sub(transferBatches.offset, 32)) + + // connectorCalls + connectorCalls.offset := add(add(offsetToCallbackData, 32), calldataload(add(offsetToCallbackData, 32))) + connectorCalls.length := calldataload(sub(connectorCalls.offset, 32)) + + // tokens + tokens.offset := add(add(offsetToCallbackData, 32), calldataload(add(offsetToCallbackData, 64))) + tokens.length := calldataload(sub(tokens.offset, 32)) + tokensOffset := sub(tokens.offset, 32) + } + ctx.tokensOffset = tokensOffset; + ctx.tokens = tokens; + + _processTransferBatches(ctx, transferBatches); + _processBridgeCalls(ctx, connectorCalls); + } + + function _processTransferBatches( + Context memory ctx, + TransferBatch[] calldata batches + ) + internal + { + for (uint o = 0; o < batches.length; o++) { + _processTransferBatch( + ctx, + batches[o] + ); + } + } + + function _processTransferBatch( + Context memory ctx, + TransferBatch calldata batch + ) + internal + { + uint96[] memory amounts = batch.amounts; + + // Verify transfers + bytes memory transfers = new bytes(amounts.length * 34); + assembly { + transfers := add(transfers, 32) + } + + for (uint i = 0; i < amounts.length; i++) { + uint targetAmount = amounts[i]; + + (uint packedData, address to, ) = readTransfer(ctx); + uint tokenID = (packedData >> 88) & 0xffff; + uint amount = (packedData >> 64) & 0xffffff; + uint fee = (packedData >> 32) & 0xffff; + // Decode floats + amount = (amount & 524287) * (10 ** (amount >> 19)); + fee = (fee & 2047) * (10 ** (fee >> 11)); + + // Verify the transaction data + require( + // txType == ExchangeData.TransactionType.TRANSFER && + // transfer.type == 1 && + // transfer.fromAccountID == ctx.accountID && + // transfer.toAccountID == UNKNOWN && + packedData & 0xffffffffffff0000000000000000000000000000000000 == + (uint(ExchangeData.TransactionType.TRANSFER) << 176) | (1 << 168) | (uint(accountID) << 136) && + /*feeTokenID*/(packedData >> 48) & 0xffff == tokenID && + fee <= (amount * MAX_FEE_BIPS / 10000) && + (100000 - 8) * targetAmount <= 100000 * amount && amount <= targetAmount, + "INVALID_BRIDGE_TRANSFER_TX_DATA" + ); + + // Pack the transfer data to compare against batch deposit hash + assembly { + mstore(add(transfers, 2), tokenID) + mstore( transfers , or(shl(96, to), targetAmount)) + transfers := add(transfers, 34) + } + } + + // Get the original transfers ptr back + assembly { + transfers := sub(transfers, add(32, mul(34, mload(amounts)))) + } + // Check if these transfers can be processed + bytes32 hash = _hashTransferBatch(transfers); + require(!_arePendingTransfersTooOld(batch.batchID, hash), "TRANSFERS_TOO_OLD"); + + // Mark transfers as completed + delete pendingTransfers[batch.batchID][hash]; + } + + function _processBridgeCalls( + Context memory ctx, + ConnectorCalls[] calldata connectorCalls + ) + internal + { + // Total amounts transferred to the bridge + uint[] memory totalAmounts = new uint[](ctx.tokens.length); + + // All resulting deposits from all bridge calls + BridgeTransfer[][] memory deposits = new BridgeTransfer[][](connectorCalls.length); + + // Verify and execute bridge calls + for (uint c = 0; c < connectorCalls.length; c++) { + ConnectorCalls calldata connectorCall = connectorCalls[c]; + + // Verify the transactions + _processConnectorCall( + ctx, + connectorCall, + totalAmounts + ); + + // Call the connector + deposits[c] = _connectorCall( + ctx, + connectorCall, + c, + connectorCalls + ); + } + + // Verify withdrawals + _processWithdrawals(ctx, totalAmounts); + + // Do all resulting transfers back from the bridge to the users + _batchDeposit(address(this), deposits); + } + + function _processConnectorCall( + Context memory ctx, + ConnectorCalls calldata connectorCall, + uint[] memory totalAmounts + ) + internal + view + { + CallTransfer memory transfer; + uint totalMinGas = 0; + for (uint g = 0; g < connectorCall.groups.length; g++) { + ConnectorGroup calldata group = connectorCall.groups[g]; + for (uint i = 0; i < group.calls.length; i++) { + BridgeCall calldata bridgeCall = group.calls[i]; + + // packedData: txType (1) | type (1) | fromAccountID (4) | toAccountID (4) | tokenID (2) | amount (3) | feeTokenID (2) | fee (2) | storageID (4) + (uint packedData, , ) = readTransfer(ctx); + transfer.fromAccountID = (packedData >> 136) & 0xffffffff; + transfer.tokenID = (packedData >> 88) & 0xffff; + transfer.amount = (packedData >> 64) & 0xffffff; + transfer.feeTokenID = (packedData >> 48) & 0xffff; + transfer.fee = (packedData >> 32) & 0xffff; + transfer.storageID = (packedData ) & 0xffffffff; + + transfer.amount = (transfer.amount & 524287) * (10 ** (transfer.amount >> 19)); + transfer.fee = (transfer.fee & 2047) * (10 ** (transfer.fee >> 11)); + + // Verify that the transaction was approved with an L2 signature + bytes32 txHash = _hashTx( + transfer, + bridgeCall.maxFee, + bridgeCall.validUntil, + bridgeCall.minGas, + connectorCall.connector, + group.groupData, + bridgeCall.userData + ); + verifySignatureL2(ctx, bridgeCall.owner, transfer.fromAccountID, txHash); + + // Find the token in the tokens list + uint t = 0; + while (t < ctx.tokens.length && transfer.tokenID != ctx.tokens[t].tokenID) { + t++; + } + require(t < ctx.tokens.length, "INVALID_INPUT_TOKENS"); + totalAmounts[t] += transfer.amount; + + // Verify the transaction data + require( + // txType == ExchangeData.TransactionType.TRANSFER && + // transfer.type == 1 && + // transfer.fromAccountID == UNKNOWN && + // transfer.toAccountID == ctx.accountID && + packedData & 0xffff00000000ffffffff00000000000000000000000000 == + (uint(ExchangeData.TransactionType.TRANSFER) << 176) | (1 << 168) | (uint(accountID) << 104) && + transfer.fee <= bridgeCall.maxFee && + bridgeCall.validUntil == 0 || block.timestamp < bridgeCall.validUntil && + bridgeCall.token == ctx.tokens[t].token && + bridgeCall.amount == transfer.amount, + "INVALID_BRIDGE_CALL_TRANSFER" + ); + + totalMinGas = totalMinGas.add(bridgeCall.minGas); + } + } + + // Make sure the gas passed to the connector is at least the sum of all call gas min amounts. + // So calls basically "buy" a part of the total gas needed to do the batched call, + // while IBridgeConnector.getMinGasLimit() makes sure the total gas limit makes sense for the + // amount of work submitted. + require(connectorCall.gasLimit >= totalMinGas, "INVALID_TOTAL_MIN_GAS"); + } + + function _processWithdrawals( + Context memory ctx, + uint[] memory totalAmounts + ) + internal + { + // Verify the withdrawals + for (uint i = 0; i < ctx.tokens.length; i++) { + TokenData memory token = ctx.tokens[i]; + // Verify token data + require( + _getTokenID(token.token) == token.tokenID && + token.amount == totalAmounts[i], + "INVALID_TOKEN_DATA" + ); + + bytes20 onchainDataHash = WithdrawTransaction.hashOnchainData( + 0, // Withdrawal needs to succeed no matter the gas coast + address(this), // Withdraw to this contract first + new bytes(0) + ); + + // Verify withdrawal data + uint txsDataPtr = ctx.txsDataPtr - 21; + uint header; + uint packedData; + bytes20 dataHash; + assembly { + header := calldataload( txsDataPtr ) + packedData := calldataload(add(txsDataPtr, 42)) + dataHash := and(calldataload(add(txsDataPtr, 78)), 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000) + } + require( + // txType == ExchangeData.TransactionType.WITHDRAWAL && + // withdrawal.type == 1 && + header & 0xffff == (uint(ExchangeData.TransactionType.WITHDRAWAL) << 8) | 1 && + // withdrawal.tokenID == token.tokenID && + // withdrawal.amount == token.amount && + // withdrawal.fee == 0, + packedData & 0xffffffffffffffffffffffffffff0000ffff == (uint(token.tokenID) << 128) | (token.amount << 32) && + onchainDataHash == dataHash, + "INVALID_BRIDGE_WITHDRAWAL_TX_DATA" + ); + + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; + } + } + + function _storeTransfers( + bytes memory transfers, + address from + ) + internal + { + uint batchID = batchIDGenerator++; + + // Store transfers to distribute at a later time + bytes32 hash = _hashTransferBatch(transfers); + require(pendingTransfers[batchID][hash] == 0, "DUPLICATE_BATCH"); + pendingTransfers[batchID][hash] = block.timestamp; + + // Log transfers to do + emit Transfers(batchID, transfers, from); + } + + function _deposit( + address from, + address token, + uint96 amount + ) + internal + { + if (amount == 0) { + return; + } + + if (from == address(this) && token != address(0)) { + ERC20(token).approve(address(depositContract), amount); + } + // Do the token transfer directly to the exchange + uint ethValue = (token == address(0)) ? amount : 0; + exchange.deposit{value: ethValue}(from, address(this), token, amount, new bytes(0)); + } + + function _connectorCall( + Context memory ctx, + ConnectorCalls calldata connectorCalls, + uint n, + ConnectorCalls[] calldata allConnectorCalls + ) + internal + returns (BridgeTransfer[] memory transfers) + { + require(connectorCalls.connector != address(this), "INVALID_CONNECTOR"); + require(trustedConnectors[connectorCalls.connector], "ONLY_TRUSTED_CONNECTORS_SUPPORTED"); + + // Check if the minimum amount of gas required is achieved + bytes memory txData = _getConnectorCallData(ctx, IBridgeConnector.getMinGasLimit.selector, allConnectorCalls, n); + (bool success, bytes memory returnData) = connectorCalls.connector.fastCall(GAS_LIMIT_CHECK_GAS_LIMIT, 0, txData); + if (success) { + require(connectorCalls.gasLimit >= abi.decode(returnData, (uint)), "GAS_LIMIT_TOO_LOW"); + } else { + // If the call failed for some reason just continue. + } + + // Execute the logic using a delegate so no extra transfers are needed + txData = _getConnectorCallData(ctx,IBridgeConnector.processCalls.selector, allConnectorCalls, n); + (success, returnData) = connectorCalls.connector.fastDelegatecall(connectorCalls.gasLimit, txData); + + if (success) { + emit ConnectorCallResult(connectorCalls.connector, true, ""); + transfers = abi.decode(returnData, (BridgeTransfer[])); + } else { + // If the call failed return funds to all users + uint totalNumCalls = 0; + for (uint g = 0; g < connectorCalls.groups.length; g++) { + totalNumCalls += connectorCalls.groups[g].calls.length; + } + transfers = new BridgeTransfer[](totalNumCalls); + uint txIdx = 0; + for (uint g = 0; g < connectorCalls.groups.length; g++) { + ConnectorGroup memory group = connectorCalls.groups[g]; + for (uint i = 0; i < group.calls.length; i++) { + BridgeCall memory bridgeCall = group.calls[i]; + transfers[txIdx++] = BridgeTransfer({ + owner: bridgeCall.owner, + token: bridgeCall.token, + amount: bridgeCall.amount + }); + } + } + assert(txIdx == totalNumCalls); + emit ConnectorCallResult(connectorCalls.connector, false, returnData); + } + } + + // Returns the tokenID for the given token address. + // Instead of querying the exchange each time, the tokenID + // is automatically cached inside this contract to save gas. + function _getTokenID(address tokenAddress) + internal + returns (uint16 cachedTokenID) + { + if (tokenAddress == address(0)) { + cachedTokenID = 0; + } else { + cachedTokenID = cachedTokenIDs[tokenAddress]; + if (cachedTokenID == 0) { + cachedTokenID = exchange.getTokenID(tokenAddress); + cachedTokenIDs[tokenAddress] = cachedTokenID; + } + } + } + + function _transferOut( + address token, + uint amount, + address to + ) + internal + { + if (token == address(0)) { + to.sendETHAndVerify(amount, gasleft()); + } else { + token.safeTransferAndVerify(to, amount); + } + } + + function _hashTransferBatch( + bytes memory transfers + ) + internal + pure + returns (bytes32) + { + return keccak256(transfers); + } + + function _arePendingTransfersTooOld(uint batchID, bytes32 hash) + internal + view + returns (bool) + { + uint timestamp = pendingTransfers[batchID][hash]; + require(timestamp != 0, "UNKNOWN_TRANSFERS"); + return block.timestamp > timestamp + MAX_AGE_PENDING_TRANSFER; + } + + function _hashTx( + CallTransfer memory transfer, + uint maxFee, + uint validUntil, + uint minGas, + address connector, + bytes memory groupData, + bytes memory userData + ) + internal + view + returns (bytes32 h) + { + bytes32 _DOMAIN_SEPARATOR = DOMAIN_SEPARATOR; + uint tokenID = transfer.tokenID; + uint amount = transfer.amount; + uint feeTokenID = transfer.feeTokenID; + uint storageID = transfer.storageID; + + /*return EIP712.hashPacked( + _DOMAIN_SEPARATOR, + keccak256( + abi.encode( + BRIDGE_CALL_TYPEHASH, + tokenID, + amount, + feeTokenID, + storageID, + minGas, + connector, + keccak256(groupData), + keccak256(userData) + ) + ) + );*/ + bytes32 typeHash = BRIDGE_CALL_TYPEHASH; + assembly { + let data := mload(0x40) + mstore( data , typeHash) + mstore(add(data, 32), tokenID) + mstore(add(data, 64), amount) + mstore(add(data, 96), feeTokenID) + mstore(add(data, 128), maxFee) + mstore(add(data, 160), validUntil) + mstore(add(data, 192), storageID) + mstore(add(data, 224), minGas) + mstore(add(data, 256), connector) + mstore(add(data, 288), keccak256(add(groupData, 32), mload(groupData))) + mstore(add(data, 320), keccak256(add(userData , 32), mload(userData))) + let p := keccak256(data, 352) + mstore(data, "\x19\x01") + mstore(add(data, 2), _DOMAIN_SEPARATOR) + mstore(add(data, 34), p) + h := keccak256(data, 66) + } + } + + function _getConnectorCallData( + Context memory ctx, + bytes4 selector, + ConnectorCalls[] calldata calls, + uint n + ) + internal + pure + returns (bytes memory) + { + // Position in the calldata to start copying + uint offsetToGroups; + ConnectorGroup[] calldata groups = calls[n].groups; + assembly { + offsetToGroups := sub(groups.offset, 32) + } + + // Amount of bytes that need to be copied. + // Found by either using the offset to the next connector call or (for the last call) + // using the offset of the data after all calls (which is the tokens array). + uint txDataSize = 0; + if (n + 1 < calls.length) { + uint offsetToCall; + uint offsetToNextCall; + assembly { + offsetToCall := calldataload(add(calls.offset, mul(add(n, 0), 32))) + offsetToNextCall := calldataload(add(calls.offset, mul(add(n, 1), 32))) + } + txDataSize = offsetToNextCall.sub(offsetToCall); + } else { + txDataSize = ctx.tokensOffset.sub(offsetToGroups); + } + + // Create the calldata for the call + bytes memory txData = new bytes(4 + 32 + txDataSize); + assembly { + mstore(add(txData, 32), selector) + mstore(add(txData, 36), 0x20) + calldatacopy(add(txData, 68), offsetToGroups, txDataSize) + } + + return txData; + } + + function readTransfer(Context memory ctx) + internal + pure + returns (uint packedData, address to, address from) + { + uint txsDataPtr = ctx.txsDataPtr; + // packedData: txType (1) | type (1) | fromAccountID (4) | toAccountID (4) | tokenID (2) | amount (3) | feeTokenID (2) | fee (2) | storageID (4) + assembly { + packedData := calldataload(txsDataPtr) + to := and(calldataload(add(txsDataPtr, 20)), 0xffffffffffffffffffffffffffffffffffffffff) + from := and(calldataload(add(txsDataPtr, 40)), 0xffffffffffffffffffffffffffffffffffffffff) + } + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; + } + + function verifySignatureL2( + Context memory ctx, + address owner, + uint _accountID, + bytes32 txHash + ) + internal + pure + { + // Read the signature verification transaction + uint txsDataPtr = ctx.txsDataPtr + 2; + uint packedData; + uint data; + assembly { + packedData := calldataload(txsDataPtr) + data := calldataload(add(txsDataPtr, 32)) + } + + // Verify that the hash was signed on L2 + require( + packedData & 0xffffffffffffffffffffffffffffffffffffffffffffffffff == + (uint(ExchangeData.TransactionType.SIGNATURE_VERIFICATION) << 192) | ((uint(owner) & 0x00ffffffffffffffffffffffffffffffffffffffff) << 32) | _accountID && + data == uint(txHash) >> 3, + "INVALID_OFFCHAIN_L2_APPROVAL" + ); + + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; + } + + function encode(BridgeOperations calldata operations) + external + pure + {} +} diff --git a/packages/loopring_v3/contracts/aux/bridge/IBridge.sol b/packages/loopring_v3/contracts/aux/bridge/IBridge.sol new file mode 100644 index 000000000..5244ffe85 --- /dev/null +++ b/packages/loopring_v3/contracts/aux/bridge/IBridge.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +struct BridgeCall +{ + address owner; + address token; + uint96 amount; + bytes userData; + uint minGas; + uint maxFee; + uint validUntil; +} + +struct ConnectorGroup +{ + bytes groupData; + BridgeCall[] calls; +} + +struct BridgeTransfer +{ + address owner; + address token; + uint96 amount; +} + +/// @title IBridge interface +/// @author Brecht Devos - +interface IBridge +{ + /// @dev Optimized L1 -> L2 path. Allows doing many deposits in an efficient way. + /// + /// Every normal deposit to Loopring exchange does a real L1 token transfer + /// and stores some data on-chain costing ~65k gas. + /// This function batches all deposits togeter and only does a single exchange + /// deposit for each distinct token. All deposits are then handled by L2 transfers + /// instead of L1 transfers, which makes them much cheaper. + /// + /// The sender will send the funds to Loopring exchange, so just like with normal + /// deposits the sender first has to approve token transfers on the deposit contract. + /// + /// @param deposits The deposits + function batchDeposit(BridgeTransfer[] calldata deposits) + external + payable; +} + +/// @title IBridgeConnector interface +/// @author Brecht Devos - +interface IBridgeConnector +{ + /// @dev Optimized L2 -> L1 (-> L2) path. Allows interacting with L1 dApps in an efficient way. + /// + /// For a user to interact with L1 the user normally needs to first withdraw and then + /// do a normal L1 transaction. And if the user then also wants to move back to L2 a deposit + /// is necessary again. With high gas prices this can get expensive. + /// + /// The bridge allows batching expensive L1 work between users: + /// - All withdrawals are reduced to just a single withdrawal per distinct token for all bridge operations + /// - The L1 transaction itself (if the operation allows for this) can be shared between all users + /// that want to do the same operation. + /// - All deposits back to L2 are also reduced to just a single deposit per distinct token for all bridge operations + /// + /// Most of this is abstracted away in the bridge. A user sings a BridgeCall and `processCalls` + /// gets a list of bridge calls divided in lists based on `groupData` + /// (e.g. for a uniswap connector the group would be the 2 tokens being traded). + /// Each bridge call contains how much each user transfered to the bridge to be used for the specific bridge call. + /// The bridge call also contain a user specific `userData` which can contain per user parameters (e.g. for + /// uniswap the allowd slippage, for mass migration the destination address,...). + /// In some cases the interaction results in new tokens that the user wants to receive back on L2. To allow this + /// the function returns a list of transfers that need to be done from the bridge back to the users (which would + /// be similar to just calling IBridge.batchDeposit(), but by returning the list here more optimizations are possible + /// between different connector calls). + /// + /// @param groups The groups of bridge calls to process + function processCalls(ConnectorGroup[] calldata groups) + external + payable + returns (BridgeTransfer[] memory); + + /// @dev Returns a rough estimate of the gas cost to do `processCalls`. At least this much gas needs to be + /// provided by the caller of `processCalls` before the BridgeCalls of users are allowed to be used. + /// + /// Aach bridge call only pays for a small part of the necessary total gas consumed by a + /// a connector call. As such, the caller of `processCalls` would easily be able to just let all + /// `processCalls` calls fail by e.g. not batching enough Bridge calls together (while still collecting the fee). + /// + /// @param groups The groups of bridge calls to process + function getMinGasLimit(ConnectorGroup[] calldata groups) + external + pure + returns (uint); +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/aux/transactions/TransactionReader.sol b/packages/loopring_v3/contracts/aux/transactions/TransactionReader.sol index 35744f504..5eb50d1af 100644 --- a/packages/loopring_v3/contracts/aux/transactions/TransactionReader.sol +++ b/packages/loopring_v3/contracts/aux/transactions/TransactionReader.sol @@ -104,12 +104,44 @@ library TransactionReader { internal pure { - bytes memory txData = txsData; + require(txIdx + numTransactions <= _block.blockSize, "INVALID_TX_RANGE"); + uint TX_DATA_AVAILABILITY_SIZE = ExchangeData.TX_DATA_AVAILABILITY_SIZE; + uint TX_DATA_AVAILABILITY_SIZE_PART_1 = ExchangeData.TX_DATA_AVAILABILITY_SIZE_PART_1; + uint TX_DATA_AVAILABILITY_SIZE_PART_2 = ExchangeData.TX_DATA_AVAILABILITY_SIZE_PART_2; + + // Part 1 + uint offset = BlockReader.OFFSET_TO_TRANSACTIONS + + txIdx * TX_DATA_AVAILABILITY_SIZE_PART_1; + bytes memory data1 = _block.data; + assembly { + data1 := add(data1, add(offset, 32)) + } + + // Part 2 + offset = BlockReader.OFFSET_TO_TRANSACTIONS + + _block.blockSize * TX_DATA_AVAILABILITY_SIZE_PART_1 + + txIdx * TX_DATA_AVAILABILITY_SIZE_PART_2; + bytes memory data2 = _block.data; + assembly { + data2 := add(data2, add(offset, 32)) + } + + // Add fixed offset once + assembly { + txsData := add(txsData, 32) + } + + // Read the transactions for (uint i = 0; i < numTransactions; i++) { - _block.data.readTransactionData(txIdx + i, _block.blockSize, txData); assembly { - txData := add(txData, TX_DATA_AVAILABILITY_SIZE) + mstore( txsData , mload( data1 )) + mstore(add(txsData, 29), mload( data2 )) + mstore(add(txsData, 36), mload(add(data2, 7))) + + txsData := add(txsData, TX_DATA_AVAILABILITY_SIZE) + data1 := add(data1 , TX_DATA_AVAILABILITY_SIZE_PART_1) + data2 := add(data2 , TX_DATA_AVAILABILITY_SIZE_PART_2) } } } diff --git a/packages/loopring_v3/contracts/converters/BaseConverter.sol b/packages/loopring_v3/contracts/converters/BaseConverter.sol new file mode 100644 index 000000000..96ce17df0 --- /dev/null +++ b/packages/loopring_v3/contracts/converters/BaseConverter.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; + +import "../core/iface/IExchangeV3.sol"; +import "../lib/Claimable.sol"; +import "../lib/Drainable.sol"; +import "../lib/ERC20.sol"; +import "../lib/AddressUtil.sol"; +import "../lib/ERC20SafeTransfer.sol"; +import "../lib/MathUint.sol"; +import "../lib/LPERC20.sol"; + + +/// @author Brecht Devos - +abstract contract BaseConverter is LPERC20, Claimable, Drainable +{ + using AddressUtil for address; + using ERC20SafeTransfer for address; + using MathUint for uint; + + event ConversionSuccess (uint amountIn, uint amountOut); + event ConversionFailed (string reason); + + IExchangeV3 public immutable exchange; + IDepositContract public immutable depositContract; + + bool public initialized; + + address public tokenIn; + address public tokenOut; + + bool public failed; + + modifier onlyFromExchangeOwner() + { + require(msg.sender == exchange.owner(), "UNAUTHORIZED"); + _; + } + + constructor( + IExchangeV3 _exchange + ) + { + exchange = _exchange; + depositContract = _exchange.getDepositContract(); + } + + function initialize( + string memory _name, + string memory _symbol, + uint8 _decimals, + address _tokenIn, + address _tokenOut + ) + external + { + require(!initialized, "ALREADY_INITIALIZED"); + initializeToken(_name, _symbol, _decimals); + + tokenIn = _tokenIn; + tokenOut = _tokenOut; + + initialized = true; + } + + function deposit( + uint96 amountIn, + uint96 minAmountOut, + bytes calldata customData + ) + external + payable + onlyFromExchangeOwner + { + require(totalSupply == 0); + + // Converter specific logic, which can fail + try BaseConverter(this).convertExternal(amountIn, minAmountOut, customData) + returns (uint amountOut) { + failed = false; + emit ConversionSuccess(amountIn, amountOut); + } catch Error(string memory reason) { + failed = true; + emit ConversionFailed(reason); + } catch { + failed = true; + emit ConversionFailed("unknown"); + } + + // Mint pool tokens representing each user's share in the pool, with 1:1 ratio + _mint(address(this), amountIn); + + // Repay the flash mint used to give user's their share on L2 + _repay(address(this), amountIn); + } + + function withdraw( + address to, + uint96 poolAmount, + uint96 repayAmount + ) + public + { + // Token to withdraw + address token = failed ? tokenIn : tokenOut; + + // Current balance in the contract + uint balance = 0; + if (token == address(0)) { + balance = address(this).balance; + } else { + balance = ERC20(token).balanceOf(address(this)); + } + + // Share to withdraw + uint amount = balance.mul(poolAmount) / totalSupply; + + // Burn pool tokens + _burn(msg.sender, poolAmount); + + // Use to repay flash mint directly if requested + if (repayAmount > 0) { + _repay(token, repayAmount); + } + + // Send remaining amount to `to` + uint amountToSend = amount.sub(repayAmount); + if (token == address(0)) { + to.sendETHAndVerify(amountToSend, gasleft()); // ETH + } else { + token.safeTransferAndVerify(to, amountToSend); // ERC20 token + } + } + + // Wrapper around `convert` which enforces only self calls. + function convertExternal( + uint96 amountIn, + uint96 minAmountOut, + bytes calldata customData + ) + external + virtual + returns (uint amountOut) + { + require(msg.sender == address(this), "UNAUTHORIZED"); + amountOut = convert(amountIn, minAmountOut, customData); + } + + receive() + external + payable + {} + + function _repay( + address token, + uint96 amount + ) + private + { + uint ethValue = (token == address(0)) ? amount : 0; + IExchangeV3(exchange).repayFlashMint{value: ethValue}( + address(this), + token, + amount, + new bytes(0) + ); + } + + // Function to approve tokens so this doesn't have to be done every time the conversion is done + function approveTokens() + public + virtual + { + if (tokenIn != address(0)) { + ERC20(tokenIn).approve(address(depositContract), type(uint256).max); + } + if (tokenOut != address(0)) { + ERC20(tokenOut).approve(address(depositContract), type(uint256).max); + } + ERC20(address(this)).approve(address(depositContract), type(uint256).max); + } + + function canDrain(address drainer, address /* token */) + public + override + view + returns (bool) + { + return drainer == owner && totalSupply == 0; + } + + // Converer specific logic + function convert( + uint96 amountIn, + uint96 minAmountOut, + bytes calldata customData + ) + internal + virtual + returns (uint amountOut); +} diff --git a/packages/loopring_v3/contracts/core/iface/ExchangeData.sol b/packages/loopring_v3/contracts/core/iface/ExchangeData.sol index aa67f0f3f..da2845c0d 100644 --- a/packages/loopring_v3/contracts/core/iface/ExchangeData.sol +++ b/packages/loopring_v3/contracts/core/iface/ExchangeData.sol @@ -158,6 +158,13 @@ library ExchangeData uint[24] balanceMerkleProof; } + struct FlashMint + { + address to; + address token; + uint96 amount; + } + struct BlockContext { bytes32 DOMAIN_SEPARATOR; @@ -224,5 +231,15 @@ library ExchangeData // Last time the protocol fee was withdrawn for a specific token mapping (address => uint) protocolFeeLastWithdrawnTime; + + // Duplicated loopring address + address loopringAddr; + // AMM fee bips + uint8 ammFeeBips; + // Enable/Disable `onchainTransferFrom` + bool allowOnchainTransferFrom; + + // Flash mints + mapping (address => uint96) amountFlashMinted; } } diff --git a/packages/loopring_v3/contracts/core/iface/IExchangeV3.sol b/packages/loopring_v3/contracts/core/iface/IExchangeV3.sol index e6c6a1a7e..8e2135cad 100644 --- a/packages/loopring_v3/contracts/core/iface/IExchangeV3.sol +++ b/packages/loopring_v3/contracts/core/iface/IExchangeV3.sol @@ -343,6 +343,53 @@ abstract contract IExchangeV3 is Claimable view returns (uint96); + + /// @dev Flash mints tokens on L2. + /// The amount minted has to be repaid using `repayFlashMint`. + /// + /// This function is only callable by the owner. + /// + /// @param flashMints The list of flash mints to be done. + function flashMint( + ExchangeData.FlashMint[] calldata flashMints + ) + external + virtual; + + /// @dev Repays funds minted using `flashMint`. + /// @param from The address that deposits the funds to the exchange + /// @param tokenAddress The address of the token, use `0x0` for Ether. + /// @param amount The amount of tokens to deposit + /// @param extraData Optional extra data used by the deposit contract + function repayFlashMint( + address from, + address tokenAddress, + uint96 amount, + bytes calldata extraData + ) + external + virtual + payable; + + /// @dev Verifies all minted tokens were paid back. + /// @param flashMints The list of flash mints that were done. + function verifyFlashMintsPaidBack( + ExchangeData.FlashMint[] calldata flashMints + ) + external + virtual + view; + + /// @dev Returns the amount flash minted for a specific token. + /// @param tokenAddress The token + function getAmountFlashMinted( + address tokenAddress + ) + external + virtual + view + returns (uint96); + // -- Withdrawals -- /// @dev Submits an onchain request to force withdraw Ether or ERC20 tokens. /// This request always withdraws the full balance. diff --git a/packages/loopring_v3/contracts/core/impl/ExchangeV3.sol b/packages/loopring_v3/contracts/core/impl/ExchangeV3.sol index 374bff511..b29642839 100644 --- a/packages/loopring_v3/contracts/core/impl/ExchangeV3.sol +++ b/packages/loopring_v3/contracts/core/impl/ExchangeV3.sol @@ -42,15 +42,12 @@ contract ExchangeV3 is IExchangeV3, ReentrancyGuard using ExchangeTokens for ExchangeData.State; using ExchangeWithdrawals for ExchangeData.State; - ExchangeData.State public state; - address public loopringAddr; - uint8 private ammFeeBips = 20; - bool public allowOnchainTransferFrom = false; + ExchangeData.State private state; modifier onlyWhenUninitialized() { require( - loopringAddr == address(0) && state.merkleRoot == bytes32(0), + state.loopringAddr == address(0) && state.merkleRoot == bytes32(0), "INITIALIZED" ); _; @@ -94,7 +91,8 @@ contract ExchangeV3 is IExchangeV3, ReentrancyGuard { require(address(0) != _owner, "ZERO_ADDRESS"); owner = _owner; - loopringAddr = _loopring; + state.loopringAddr = _loopring; + state.ammFeeBips = 20; state.initializeGenesisBlock( _loopring, @@ -368,7 +366,7 @@ contract ExchangeV3 is IExchangeV3, ReentrancyGuard nonReentrant onlyFromUserOrAgent(from) { - state.deposit(from, to, tokenAddress, amount, extraData); + state.deposit(from, to, tokenAddress, amount, extraData, false); } function getPendingDepositAmount( @@ -384,6 +382,66 @@ contract ExchangeV3 is IExchangeV3, ReentrancyGuard return state.pendingDeposits[owner][tokenID].amount; } + function flashMint( + ExchangeData.FlashMint[] calldata flashMints + ) + external + override + nonReentrant + onlyOwner + { + for (uint i = 0; i < flashMints.length; i++) { + state.deposit( + flashMints[i].to, + flashMints[i].to, + flashMints[i].token, + flashMints[i].amount, + new bytes(0), + true + ); + } + } + + function repayFlashMint( + address from, + address tokenAddress, + uint96 amount, + bytes calldata extraData + ) + external + payable + override + nonReentrant + { + state.repayFlashMint(from, tokenAddress, amount, extraData); + } + + function getAmountFlashMinted( + address tokenAddress + ) + external + override + view + returns (uint96) + { + return state.amountFlashMinted[tokenAddress]; + } + + function verifyFlashMintsPaidBack( + ExchangeData.FlashMint[] calldata flashMints + ) + external + override + view + { + for (uint i = 0; i < flashMints.length; i++) { + require( + state.amountFlashMinted[flashMints[i].token] == 0, + "FLASH_MINT_NOT_REPAID" + ); + } + } + // -- Withdrawals -- function forceWithdraw( @@ -556,7 +614,7 @@ contract ExchangeV3 is IExchangeV3, ReentrancyGuard nonReentrant onlyFromUserOrAgent(from) { - require(allowOnchainTransferFrom, "NOT_ALLOWED"); + require(state.allowOnchainTransferFrom, "NOT_ALLOWED"); state.depositContract.transfer(from, to, token, amount); } @@ -671,15 +729,16 @@ contract ExchangeV3 is IExchangeV3, ReentrancyGuard onlyOwner { require(_feeBips <= 200, "INVALID_VALUE"); - ammFeeBips = _feeBips; + state.ammFeeBips = _feeBips; } function getAmmFeeBips() external override view - returns (uint8) { - return ammFeeBips; + returns (uint8) + { + return state.ammFeeBips; } function setAllowOnchainTransferFrom(bool value) @@ -687,7 +746,7 @@ contract ExchangeV3 is IExchangeV3, ReentrancyGuard nonReentrant onlyOwner { - require(allowOnchainTransferFrom != value, "SAME_VALUE"); - allowOnchainTransferFrom = value; + require(state.allowOnchainTransferFrom != value, "SAME_VALUE"); + state.allowOnchainTransferFrom = value; } } diff --git a/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeBlocks.sol b/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeBlocks.sol index 967aa0602..eeaffbe91 100644 --- a/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeBlocks.sol +++ b/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeBlocks.sol @@ -195,17 +195,19 @@ library ExchangeBlocks private { if (header.numConditionalTransactions > 0) { - // Cache the domain seperator to save on SLOADs each time it is accessed. + // Cache the domain separator to save on SLOADs each time it is accessed. ExchangeData.BlockContext memory ctx = ExchangeData.BlockContext({ DOMAIN_SEPARATOR: S.DOMAIN_SEPARATOR, timestamp: header.timestamp }); ExchangeData.AuxiliaryData[] memory block_auxiliaryData; + { bytes memory blockAuxData = _block.auxiliaryData; assembly { block_auxiliaryData := add(blockAuxData, 64) } + } require( block_auxiliaryData.length == header.numConditionalTransactions, @@ -215,20 +217,23 @@ library ExchangeBlocks // Run over all conditional transactions uint minTxIndex = 0; bytes memory txData = new bytes(ExchangeData.TX_DATA_AVAILABILITY_SIZE); + + uint txIndex; + bool approved; + bytes memory auxData; + ExchangeData.TransactionType txType; + uint offset; for (uint i = 0; i < block_auxiliaryData.length; i++) { // Load the data from auxiliaryData, which is still encoded as calldata - uint txIndex; - bool approved; - bytes memory auxData; assembly { // Offset to block_auxiliaryData[i] - let auxOffset := mload(add(block_auxiliaryData, add(32, mul(32, i)))) + offset := mload(add(block_auxiliaryData, add(32, mul(32, i)))) // Load `txIndex` (pos 0) and `approved` (pos 1) in block_auxiliaryData[i] - txIndex := mload(add(add(32, block_auxiliaryData), auxOffset)) - approved := mload(add(add(64, block_auxiliaryData), auxOffset)) + txIndex := mload(add(add(32, block_auxiliaryData), offset)) + approved := mload(add(add(64, block_auxiliaryData), offset)) // Load `data` (pos 2) - let auxDataOffset := mload(add(add(96, block_auxiliaryData), auxOffset)) - auxData := add(add(32, block_auxiliaryData), add(auxOffset, auxDataOffset)) + offset := add(offset, mload(add(add(96, block_auxiliaryData), offset))) + auxData := add(add(32, block_auxiliaryData), offset) } // Each conditional transaction needs to be processed from left to right @@ -236,25 +241,23 @@ library ExchangeBlocks minTxIndex = txIndex + 1; - if (approved) { + txType = _block.data.readTransactionType(txIndex); + + if (approved && + txType != ExchangeData.TransactionType.WITHDRAWAL && + txType != ExchangeData.TransactionType.DEPOSIT) { continue; } // Get the transaction data _block.data.readTransactionData(txIndex, _block.blockSize, txData); - // Process the transaction - ExchangeData.TransactionType txType = ExchangeData.TransactionType( - txData.toUint8(0) - ); - uint txDataOffset = 0; - if (txType == ExchangeData.TransactionType.DEPOSIT) { DepositTransaction.process( S, ctx, txData, - txDataOffset, + 0, auxData ); } else if (txType == ExchangeData.TransactionType.WITHDRAWAL) { @@ -262,15 +265,16 @@ library ExchangeBlocks S, ctx, txData, - txDataOffset, - auxData + 0, + auxData, + approved ); } else if (txType == ExchangeData.TransactionType.TRANSFER) { TransferTransaction.process( S, ctx, txData, - txDataOffset, + 0, auxData ); } else if (txType == ExchangeData.TransactionType.ACCOUNT_UPDATE) { @@ -278,7 +282,7 @@ library ExchangeBlocks S, ctx, txData, - txDataOffset, + 0, auxData ); } else if (txType == ExchangeData.TransactionType.AMM_UPDATE) { @@ -286,7 +290,7 @@ library ExchangeBlocks S, ctx, txData, - txDataOffset, + 0, auxData ); } else { @@ -297,6 +301,8 @@ library ExchangeBlocks revert("UNSUPPORTED_TX_TYPE"); } } + + require(minTxIndex <= _block.blockSize, "AUXILIARYDATA_INVALID_ORDER"); } } diff --git a/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeDeposits.sol b/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeDeposits.sol index 9f1cf27bc..eb7376f7b 100644 --- a/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeDeposits.sol +++ b/packages/loopring_v3/contracts/core/impl/libexchange/ExchangeDeposits.sol @@ -34,7 +34,8 @@ library ExchangeDeposits address to, address tokenAddress, uint96 amount, // can be zero - bytes memory extraData + bytes memory extraData, + bool flash ) internal // inline call { @@ -46,26 +47,57 @@ library ExchangeDeposits uint16 tokenID = S.getTokenID(tokenAddress); - // Transfer the tokens to this contract - uint96 amountDeposited = S.depositContract.deposit{value: msg.value}( - from, - tokenAddress, - amount, - extraData - ); + uint96 amountDeposited = amount; + if (flash) { + require(msg.value == 0, "INVALID_FLASH_DEPOSIT"); + S.amountFlashMinted[tokenAddress] = S.amountFlashMinted[tokenAddress].add(amount); + } else { + // Transfer the tokens to this contract + amountDeposited = S.depositContract.deposit{value: msg.value}( + from, + tokenAddress, + amount, + extraData + ); + + emit DepositRequested( + from, + to, + tokenAddress, + tokenID, + amountDeposited + ); + } // Add the amount to the deposit request and reset the time the operator has to process it ExchangeData.Deposit memory _deposit = S.pendingDeposits[to][tokenID]; _deposit.timestamp = uint64(block.timestamp); _deposit.amount = _deposit.amount.add(amountDeposited); S.pendingDeposits[to][tokenID] = _deposit; + } + + function repayFlashMint( + ExchangeData.State storage S, + address from, + address tokenAddress, + uint96 amount, + bytes memory extraData + ) + public + { + // Make sure the token is registered + /*uint16 tokenID = */S.getTokenID(tokenAddress); - emit DepositRequested( + // Transfer the tokens to this contract + uint96 amountDeposited = S.depositContract.deposit{value: msg.value}( from, - to, tokenAddress, - tokenID, - amountDeposited + amount, + extraData ); + require(amountDeposited > 0, "INVALID_REPAY_AMOUNT"); + + // Pay back + S.amountFlashMinted[tokenAddress] = S.amountFlashMinted[tokenAddress].sub(amountDeposited); } } diff --git a/packages/loopring_v3/contracts/core/impl/libtransactions/BlockReader.sol b/packages/loopring_v3/contracts/core/impl/libtransactions/BlockReader.sol index bb3aa1671..9a84b478d 100644 --- a/packages/loopring_v3/contracts/core/impl/libtransactions/BlockReader.sol +++ b/packages/loopring_v3/contracts/core/impl/libtransactions/BlockReader.sol @@ -82,4 +82,19 @@ library BlockReader { mstore(add(txData, 68 ), mload(add(data, add(txDataOffset, 39)))) } } + + function readTransactionType( + bytes memory data, + uint txIdx + ) + internal + pure + returns (ExchangeData.TransactionType txType) + { + uint txDataOffset = OFFSET_TO_TRANSACTIONS + + txIdx * ExchangeData.TX_DATA_AVAILABILITY_SIZE_PART_1; + assembly { + txType := and(mload(add(data, add(txDataOffset, 1))), 0xff) + } + } } diff --git a/packages/loopring_v3/contracts/core/impl/libtransactions/DepositTransaction.sol b/packages/loopring_v3/contracts/core/impl/libtransactions/DepositTransaction.sol index fa2c5aec9..71c7aaa76 100644 --- a/packages/loopring_v3/contracts/core/impl/libtransactions/DepositTransaction.sol +++ b/packages/loopring_v3/contracts/core/impl/libtransactions/DepositTransaction.sol @@ -50,7 +50,7 @@ library DepositTransaction // This is done to ensure the user can do multiple deposits after each other // without invalidating work done by the exchange owner for previous deposit amounts. - require(pendingDeposit.amount >= deposit.amount, "INVALID_AMOUNT"); + require(pendingDeposit.amount >= deposit.amount, "INVALID_DEPOSIT_AMOUNT"); pendingDeposit.amount = pendingDeposit.amount.sub(deposit.amount); // If the deposit was fully consumed, reset it so the storage is freed up diff --git a/packages/loopring_v3/contracts/core/impl/libtransactions/WithdrawTransaction.sol b/packages/loopring_v3/contracts/core/impl/libtransactions/WithdrawTransaction.sol index a4f48df4d..ef214d2d3 100644 --- a/packages/loopring_v3/contracts/core/impl/libtransactions/WithdrawTransaction.sol +++ b/packages/loopring_v3/contracts/core/impl/libtransactions/WithdrawTransaction.sol @@ -73,7 +73,8 @@ library WithdrawTransaction ExchangeData.BlockContext memory ctx, bytes memory data, uint offset, - bytes memory auxiliaryData + bytes memory auxiliaryData, + bool approved ) internal { @@ -109,11 +110,13 @@ library WithdrawTransaction require(ctx.timestamp < withdrawal.validUntil, "WITHDRAWAL_EXPIRED"); require(withdrawal.fee <= withdrawal.maxFee, "WITHDRAWAL_FEE_TOO_HIGH"); - // Check appproval onchain - // Calculate the tx hash - bytes32 txHash = hashTx(ctx.DOMAIN_SEPARATOR, withdrawal); - // Check onchain authorization - S.requireAuthorizedTx(withdrawal.from, auxData.signature, txHash); + if (!approved) { + // Check appproval onchain + // Calculate the tx hash + bytes32 txHash = hashTx(ctx.DOMAIN_SEPARATOR, withdrawal); + // Check onchain authorization + S.requireAuthorizedTx(withdrawal.from, auxData.signature, txHash); + } } else if (withdrawal.withdrawalType == 2 || withdrawal.withdrawalType == 3) { // Forced withdrawals cannot make use of certain features because the // necessary data is not authorized by the account owner. diff --git a/packages/loopring_v3/contracts/lib/AddressUtil.sol b/packages/loopring_v3/contracts/lib/AddressUtil.sol index 7cdfc2ad6..274b95ddc 100644 --- a/packages/loopring_v3/contracts/lib/AddressUtil.sol +++ b/packages/loopring_v3/contracts/lib/AddressUtil.sol @@ -113,4 +113,27 @@ library AddressUtil } } } + + function fastDelegatecall( + address to, + uint gasLimit, + bytes memory data + ) + internal + returns (bool success, bytes memory returnData) + { + if (to != address(0)) { + assembly { + // Do the call + success := delegatecall(gasLimit, to, add(data, 32), mload(data), 0, 0) + // Copy the return data + let size := returndatasize() + returnData := mload(0x40) + mstore(returnData, size) + returndatacopy(add(returnData, 32), 0, size) + // Update free memory pointer + mstore(0x40, add(returnData, add(32, size))) + } + } + } } diff --git a/packages/loopring_v3/contracts/lib/ERC20.sol b/packages/loopring_v3/contracts/lib/ERC20.sol index a12ca5145..f8069894a 100644 --- a/packages/loopring_v3/contracts/lib/ERC20.sol +++ b/packages/loopring_v3/contracts/lib/ERC20.sol @@ -6,19 +6,17 @@ pragma solidity ^0.7.0; /// @title ERC20 Token Interface /// @dev see https://github.com/ethereum/EIPs/issues/20 /// @author Daniel Wang - -abstract contract ERC20 +interface ERC20 { function totalSupply() - public - virtual + external view returns (uint); function balanceOf( address who ) - public - virtual + external view returns (uint); @@ -26,8 +24,7 @@ abstract contract ERC20 address owner, address spender ) - public - virtual + external view returns (uint); @@ -35,8 +32,7 @@ abstract contract ERC20 address to, uint value ) - public - virtual + external returns (bool); function transferFrom( @@ -44,15 +40,13 @@ abstract contract ERC20 address to, uint value ) - public - virtual + external returns (bool); function approve( address spender, uint value ) - public - virtual + external returns (bool); } diff --git a/packages/loopring_v3/contracts/lib/FloatUtil.sol b/packages/loopring_v3/contracts/lib/FloatUtil.sol index 614fc7824..b2f2d6cd6 100644 --- a/packages/loopring_v3/contracts/lib/FloatUtil.sol +++ b/packages/loopring_v3/contracts/lib/FloatUtil.sol @@ -74,4 +74,36 @@ library FloatUtil require(value < 2**96, "SafeCast: value doesn\'t fit in 96 bits"); return uint96(value); } + + // Decodes a decimal float value that is encoded like `exponent | mantissa`. + // Both exponent and mantissa are in base 10. + // Decoding to an integer is as simple as `mantissa * (10 ** exponent)` + // Will throw when the decoded value overflows an uint96 + /// @param f The float value with 5 bits exponent, 11 bits mantissa + /// @return value The decoded integer value. + function decodeFloat16Unsafe( + uint f + ) + internal + pure + returns (uint) + { + return (f & 2047) * (10 ** (f >> 11)); + } + + // Decodes a decimal float value that is encoded like `exponent | mantissa`. + // Both exponent and mantissa are in base 10. + // Decoding to an integer is as simple as `mantissa * (10 ** exponent)` + // Will throw when the decoded value overflows an uint96 + /// @param f The float value with 5 bits exponent, 19 bits mantissa + /// @return value The decoded integer value. + function decodeFloat24Unsafe( + uint f + ) + internal + pure + returns (uint) + { + return (f & 524287) * (10 ** (f >> 19)); + } } diff --git a/packages/loopring_v3/contracts/lib/LPERC20.sol b/packages/loopring_v3/contracts/lib/LPERC20.sol new file mode 100644 index 000000000..f753c4029 --- /dev/null +++ b/packages/loopring_v3/contracts/lib/LPERC20.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; + +import './ERC20.sol'; +import './MathUint.sol'; +import './SignatureUtil.sol'; +import './EIP712.sol'; + + +contract LPERC20 is ERC20 +{ + using MathUint for uint; + using SignatureUtil for bytes32; + + bytes32 public DOMAIN_SEPARATOR; + string public name; + string public symbol; + uint8 public decimals; + + uint public override totalSupply; + mapping(address => uint) public override balanceOf; + mapping(address => mapping(address => uint)) public override allowance; + mapping(address => uint) public nonces; + + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + function initializeToken( + string memory _name, + string memory _symbol, + uint8 _decimals + ) + internal + { + DOMAIN_SEPARATOR = EIP712.hash(EIP712.Domain( + "LPERC20", + "1.0", + address(this) + )); + + name = _name; + symbol = _symbol; + decimals = _decimals; + } + + function approve( + address spender, + uint value + ) + external + override + returns (bool) + { + _approve(msg.sender, spender, value); + return true; + } + + function transfer( + address to, + uint value + ) + external + override + returns (bool) + { + _transfer(msg.sender, to, value); + return true; + } + + function transferFrom( + address from, + address to, + uint value + ) + external + override + returns (bool) + { + if (msg.sender != address(this) && + allowance[from][msg.sender] != uint(-1)) { + allowance[from][msg.sender] = allowance[from][msg.sender].sub(value); + } + _transfer(from, to, value); + return true; + } + + function _mint( + address to, + uint value + ) + internal + { + totalSupply = totalSupply.add(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(address(0), to, value); + } + + function _burn( + address from, + uint value + ) + internal + { + balanceOf[from] = balanceOf[from].sub(value); + totalSupply = totalSupply.sub(value); + emit Transfer(from, address(0), value); + } + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + bytes calldata signature + ) + external + { + require(deadline >= block.timestamp, 'EXPIRED'); + + bytes32 hash = EIP712.hashPacked( + DOMAIN_SEPARATOR, + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ); + + require(hash.verifySignature(owner, signature), 'INVALID_SIGNATURE'); + _approve(owner, spender, value); + } + + function _approve( + address owner, + address spender, + uint value + ) + private + { + if (spender != address(this)) { + allowance[owner][spender] = value; + emit Approval(owner, spender, value); + } + } + + function _transfer( + address from, + address to, + uint value + ) + private + { + balanceOf[from] = balanceOf[from].sub(value); + balanceOf[to] = balanceOf[to].add(value); + emit Transfer(from, to, value); + } +} diff --git a/packages/loopring_v3/contracts/test/LPERC20.sol b/packages/loopring_v3/contracts/test/LPERC20.sol deleted file mode 100644 index 82af4f0f0..000000000 --- a/packages/loopring_v3/contracts/test/LPERC20.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2017 Loopring Technology Limited. -pragma solidity ^0.7.0; - -import '../lib/ERC20.sol'; -import '../lib/MathUint.sol'; - -contract LPERC20 is ERC20 { - using MathUint for uint; - - string public constant name = 'Loopring AMM'; - string public constant symbol = 'LLT'; - uint8 public constant decimals = 18; - uint public _totalSupply; - mapping(address => uint) public _balanceOf; - mapping(address => mapping(address => uint)) public _allowance; - - event Approval(address indexed owner, address indexed spender, uint value); - event Transfer(address indexed from, address indexed to, uint value); - - function totalSupply() public virtual view override returns (uint) { - return _totalSupply; - } - - function balanceOf(address owner) public view override virtual returns (uint balance) { - return _balanceOf[owner]; - } - - function allowance(address owner, address spender) public view override returns (uint) { - return _allowance[owner][spender]; - } - - function approve(address spender, uint value) public override returns (bool) { - _approve(msg.sender, spender, value); - return true; - } - - function transfer(address to, uint value) public override returns (bool) { - _transfer(msg.sender, to, value); - return true; - } - - function transferFrom(address from, address to, uint value) public override returns (bool) { - if (_allowance[from][msg.sender] != uint(-1)) { - _allowance[from][msg.sender] = _allowance[from][msg.sender].sub(value); - } - _transfer(from, to, value); - return true; - } - - function _mint(address to, uint value) internal { - _totalSupply = _totalSupply.add(value); - _balanceOf[to] = _balanceOf[to].add(value); - emit Transfer(address(0), to, value); - } - - function _burn(address from, uint value) internal { - _balanceOf[from] = _balanceOf[from].sub(value); - _totalSupply = _totalSupply.sub(value); - emit Transfer(from, address(0), value); - } - - function _approve(address owner, address spender, uint value) private { - _allowance[owner][spender] = value; - emit Approval(owner, spender, value); - } - - function _transfer(address from, address to, uint value) private { - _balanceOf[from] = _balanceOf[from].sub(value); - _balanceOf[to] = _balanceOf[to].add(value); - emit Transfer(from, to, value); - } -} diff --git a/packages/loopring_v3/contracts/test/LzDecompressorContract.sol b/packages/loopring_v3/contracts/test/LzDecompressorContract.sol index 35374686d..48d56325e 100644 --- a/packages/loopring_v3/contracts/test/LzDecompressorContract.sol +++ b/packages/loopring_v3/contracts/test/LzDecompressorContract.sol @@ -19,6 +19,7 @@ contract LzDecompressorContract { bytes calldata data ) external + pure returns (bytes memory) { return LzDecompressor.decompress(data); diff --git a/packages/loopring_v3/contracts/test/SinglePhaseConverter.sol b/packages/loopring_v3/contracts/test/SinglePhaseConverter.sol new file mode 100644 index 000000000..c3ffd2e54 --- /dev/null +++ b/packages/loopring_v3/contracts/test/SinglePhaseConverter.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; + +import "../core/iface/IExchangeV3.sol"; +import "../lib/Claimable.sol"; +import "../lib/Drainable.sol"; +import "../lib/ERC20.sol"; + + +/// @author Brecht Devos - +contract SinglePhaseConverter is Claimable, Drainable +{ + function swapAndRepay( + address exchange, + address swapContract, + bytes calldata swapData, + address swapToken, + uint swapAmount, + address repayToken, + uint96 repayAmount + ) + public + { + // Swap + if (swapToken != address(0)) { + ERC20(swapToken).approve(swapContract, swapAmount); + } + (bool success, ) = swapContract.call(swapData); + require(success, "SWAP_FAILED"); + + // Repay + if (repayToken != address(0)) { + IDepositContract depositContract = IExchangeV3(exchange).getDepositContract(); + ERC20(repayToken).approve(address(depositContract), repayAmount); + } + uint repayValue = (repayToken == address(0)) ? repayAmount : 0; + IExchangeV3(exchange).repayFlashMint{value: repayValue}( + address(this), + repayToken, + repayAmount, + new bytes(0) + ); + } + + function canDrain(address drainer, address /* token */) + public + override + view + returns (bool) + { + return drainer == owner; + } +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/test/TestConverter.sol b/packages/loopring_v3/contracts/test/TestConverter.sol new file mode 100644 index 000000000..53ad2ca0d --- /dev/null +++ b/packages/loopring_v3/contracts/test/TestConverter.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; + +import "../converters/BaseConverter.sol"; +import "./TestSwapper.sol"; + + +/// @author Brecht Devos - +contract TestConverter is BaseConverter +{ + TestSwapper public immutable swapContract; + + constructor( + IExchangeV3 _exchange, + TestSwapper _swapContract + ) + BaseConverter(_exchange) + { + swapContract = _swapContract; + } + + function convert( + uint96 amountIn, + uint96 minAmountOut, + bytes calldata /*customData*/ + ) + internal + override + returns (uint amountOut) + { + uint ethValue = (tokenIn == address(0)) ? amountIn : 0; + amountOut = swapContract.swap{value: ethValue}(tokenIn, tokenOut, amountIn); + require(amountOut >= minAmountOut, "INSUFFICIENT_OUT_AMOUNT"); + } + + function approveTokens() + public + override + { + super.approveTokens(); + if (tokenIn != address(0)) { + ERC20(tokenIn).approve(address(swapContract), type(uint256).max); + } + } +} diff --git a/packages/loopring_v3/contracts/test/TestMigrationBridgeConnector.sol b/packages/loopring_v3/contracts/test/TestMigrationBridgeConnector.sol new file mode 100644 index 000000000..6d7ba5267 --- /dev/null +++ b/packages/loopring_v3/contracts/test/TestMigrationBridgeConnector.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "./TestSwapper.sol"; +import "../core/iface/IExchangeV3.sol"; +import "../lib/AddressUtil.sol"; +import "../lib/ERC20.sol"; +import "../lib/ERC20SafeTransfer.sol"; +import "../lib/MathUint.sol"; +import "../thirdparty/SafeCast.sol"; +import "../aux/bridge/IBridge.sol"; + + +/// Migrates from Loopring to ... Loopring! +/// @author Brecht Devos - +contract TestMigrationBridgeConnector is IBridgeConnector +{ + using AddressUtil for address payable; + using ERC20SafeTransfer for address; + using MathUint for uint; + using SafeCast for uint; + + struct GroupSettings + { + address token; + } + + struct UserSettings + { + address to; + } + + IExchangeV3 public immutable exchange; + IDepositContract public immutable depositContract; + + IBridge public immutable bridge; + + constructor( + IExchangeV3 _exchange, + IBridge _bridge + ) + { + exchange = _exchange; + depositContract = _exchange.getDepositContract(); + + bridge = _bridge; + } + + function processCalls(ConnectorGroup[] memory groups) + external + payable + override + returns (BridgeTransfer[] memory) + { + uint numTransfers = 0; + for (uint g = 0; g < groups.length; g++) { + numTransfers += groups[g].calls.length; + } + BridgeTransfer[] memory transfers = new BridgeTransfer[](numTransfers); + uint transferIdx = 0; + + // Total ETH to migrate + uint totalAmountETH = 0; + BridgeCall memory bridgeCall; + for (uint g = 0; g < groups.length; g++) { + GroupSettings memory settings = abi.decode(groups[g].groupData, (GroupSettings)); + + BridgeCall[] memory calls = groups[g].calls; + + // Check for each call if the minimum slippage was achieved + uint totalAmount = 0; + for (uint i = 0; i < calls.length; i++) { + bridgeCall = calls[i]; + require(calls[i].token == settings.token, "WRONG_TOKEN_IN_GROUP"); + + address to = bridgeCall.owner; + if(bridgeCall.userData.length == 32) { + UserSettings memory userSettings = abi.decode(bridgeCall.userData, (UserSettings)); + to = userSettings.to; + } + + transfers[transferIdx++] = BridgeTransfer({ + owner: to, + token: bridgeCall.token, + amount: bridgeCall.amount + }); + + totalAmount += bridgeCall.amount; + } + + if (settings.token == address(0)) { + totalAmountETH = totalAmountETH.add(totalAmount); + } else { + uint allowance = ERC20(settings.token).allowance(address(this), address(depositContract)); + ERC20(settings.token).approve(address(depositContract), allowance.add(totalAmount)); + } + } + + // Mass migrate + bridge.batchDeposit{value: totalAmountETH}(transfers); + + return new BridgeTransfer[](0); + } + + function getMinGasLimit(ConnectorGroup[] calldata groups) + external + pure + override + returns (uint gasLimit) + { + gasLimit = 40000; + for (uint g = 0; g < groups.length; g++) { + gasLimit += 75000 + 2500 * groups[g].calls.length; + } + } + + receive() + external + payable + {} +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/test/TestSwapper.sol b/packages/loopring_v3/contracts/test/TestSwapper.sol new file mode 100644 index 000000000..97eecf24f --- /dev/null +++ b/packages/loopring_v3/contracts/test/TestSwapper.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../lib/AddressUtil.sol"; +import "../lib/ERC20SafeTransfer.sol"; +import "../lib/MathUint.sol"; + + +/// @author Brecht Devos - +contract TestSwapper +{ + using AddressUtil for address payable; + using ERC20SafeTransfer for address; + using MathUint for uint; + + uint public immutable rate; + bool public immutable fail; + + constructor( + uint _rate, + bool _fail + ) + { + rate = _rate; + fail = _fail; + } + + function swap( + address tokenIn, + address tokenOut, + uint amountIn + ) + external + payable + returns (uint amountOut) + { + require(!fail, "FAIL_ENABLED"); + + if (tokenIn == address(0)) { + require(msg.value == amountIn, "INVALID_ETH_DEPOSIT"); + } else { + tokenIn.safeTransferFromAndVerify(msg.sender, address(this), amountIn); + } + + amountOut = getAmountOut(tokenIn, tokenOut, amountIn); + + if (tokenOut == address(0)) { + msg.sender.sendETHAndVerify(amountOut, gasleft()); + } else { + tokenOut.safeTransferAndVerify(msg.sender, amountOut); + } + } + + function getAmountOut( + address /*tokenIn*/, + address /*tokenOut*/, + uint amountIn + ) + public + view + returns (uint amountOut) + { + amountOut = amountIn.mul(rate) / 1 ether; + } + + receive() + external + payable + {} +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/test/TestSwappperBridgeConnector.sol b/packages/loopring_v3/contracts/test/TestSwappperBridgeConnector.sol new file mode 100644 index 000000000..122c9d3a4 --- /dev/null +++ b/packages/loopring_v3/contracts/test/TestSwappperBridgeConnector.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "./TestSwapper.sol"; +import "../core/iface/IExchangeV3.sol"; +import "../lib/AddressUtil.sol"; +import "../lib/ERC20.sol"; +import "../lib/ERC20SafeTransfer.sol"; +import "../lib/MathUint.sol"; +import "../thirdparty/SafeCast.sol"; +import "../aux/bridge/IBridge.sol"; + + +/// @author Brecht Devos - +contract TestSwappperBridgeConnector is IBridgeConnector +{ + using AddressUtil for address payable; + using ERC20SafeTransfer for address; + using MathUint for uint; + using SafeCast for uint; + + struct GroupSettings + { + address tokenIn; + address tokenOut; + } + + struct UserSettings + { + uint minAmountOut; + } + + TestSwapper public immutable testSwapper; + + constructor(TestSwapper _testSwapper) + { + testSwapper = _testSwapper; + } + + function processCalls(ConnectorGroup[] memory groups) + external + payable + override + returns (BridgeTransfer[] memory) + { + uint numTransfers = 0; + for (uint g = 0; g < groups.length; g++) { + numTransfers += groups[g].calls.length; + } + BridgeTransfer[] memory transfers = new BridgeTransfer[](numTransfers); + uint transferIdx = 0; + + BridgeCall memory bridgeCall; + for (uint g = 0; g < groups.length; g++) { + GroupSettings memory settings = abi.decode(groups[g].groupData, (GroupSettings)); + + BridgeCall[] memory calls = groups[g].calls; + + bool[] memory valid = new bool[](calls.length); + uint numValid = 0; + + uint amountInExpected = 0; + for (uint i = 0; i < calls.length; i++) { + bridgeCall = calls[i]; + if (bridgeCall.token == settings.tokenIn) { + valid[i] = true; + amountInExpected = amountInExpected + bridgeCall.amount; + } + } + + // Get expected output amount + uint amountOut = testSwapper.getAmountOut( + settings.tokenIn, + settings.tokenOut, + amountInExpected + ); + + // Check for each call if the minimum slippage was achieved + uint amountIn = 0; + uint ammountInInvalid = 0; + for (uint i = 0; i < calls.length; i++) { + bridgeCall = calls[i]; + if(valid[i] && bridgeCall.userData.length == 32) { + UserSettings memory userSettings = abi.decode(bridgeCall.userData, (UserSettings)); + uint userAmountOut = uint(bridgeCall.amount).mul(amountOut) / amountInExpected; + if (userAmountOut < userSettings.minAmountOut) { + valid[i] = false; + } + } + if (valid[i]) { + amountIn = amountIn.add(bridgeCall.amount); + numValid++; + } else { + ammountInInvalid = ammountInInvalid.add(bridgeCall.amount); + } + } + + // Do the actual swap + uint ethValueOut = (settings.tokenIn == address(0)) ? amountIn : 0; + if (settings.tokenIn != address(0)) { + ERC20(settings.tokenIn).approve(address(testSwapper), amountIn); + } + amountOut = testSwapper.swap{value: ethValueOut}( + settings.tokenIn, + settings.tokenOut, + amountIn + ); + + // Create transfers back to the users + for (uint i = 0; i < calls.length; i++) { + if (valid[i]) { + // Give equal share to all valid calls + transfers[transferIdx++] = BridgeTransfer({ + owner: calls[i].owner, + token: settings.tokenOut, + amount: (uint(calls[i].amount).mul(amountOut) / amountIn).toUint96() + }); + } else { + // Just transfer the tokens back + transfers[transferIdx++] = BridgeTransfer({ + owner: calls[i].owner, + token: calls[i].token, + amount: calls[i].amount + }); + } + } + } + assert(transfers.length == transferIdx); + + return transfers; + } + + function getMinGasLimit(ConnectorGroup[] calldata groups) + external + pure + override + returns (uint gasLimit) + { + gasLimit = 40000; + for (uint g = 0; g < groups.length; g++) { + gasLimit += 100000 + 2500 * groups[g].calls.length; + } + } + + receive() + external + payable + {} +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/test/ZeroDecompressorContract.sol b/packages/loopring_v3/contracts/test/ZeroDecompressorContract.sol index 74fb69b4e..b208e07d3 100644 --- a/packages/loopring_v3/contracts/test/ZeroDecompressorContract.sol +++ b/packages/loopring_v3/contracts/test/ZeroDecompressorContract.sol @@ -19,6 +19,7 @@ contract ZeroDecompressorContract { bytes calldata data ) external + pure returns (bytes memory) { return ZeroDecompressor.decompress(data, 0); diff --git a/packages/loopring_v3/contracts/thirdparty/BytesUtil.sol b/packages/loopring_v3/contracts/thirdparty/BytesUtil.sol index ad7fef7b4..23eb9598d 100644 --- a/packages/loopring_v3/contracts/thirdparty/BytesUtil.sol +++ b/packages/loopring_v3/contracts/thirdparty/BytesUtil.sol @@ -401,6 +401,27 @@ library BytesUtil { } + function toUint16UnsafeUint(bytes memory _bytes, uint _start) internal pure returns (uint) { + uint tempUint; + + assembly { + tempUint := and(mload(add(add(_bytes, 0x2), _start)), 0xffff) + } + + return tempUint; + } + + function toUint24UnsafeUint(bytes memory _bytes, uint _start) internal pure returns (uint) { + uint tempUint; + + assembly { + tempUint := and(mload(add(add(_bytes, 0x3), _start)), 0xffffff) + } + + return tempUint; + } + + function fastSHA256( bytes memory data ) diff --git a/packages/loopring_v3/deployment_mainnet_v3.6.md b/packages/loopring_v3/deployment_mainnet_v3.6.md index b0251b121..cf8f01908 100644 --- a/packages/loopring_v3/deployment_mainnet_v3.6.md +++ b/packages/loopring_v3/deployment_mainnet_v3.6.md @@ -53,13 +53,15 @@ - ~~LoopringAmmPool: 0xE6AbFcABE24F06197a7A20dc9c81c251f2862430~~ ### UPGRADE-2021-03-25 + + - [LoopringIOExchangeOwner: 0x153CdDD727e407Cb951f728F24bEB9A5FaaA8512](https://etherscan.io/address/0x153CdDD727e407Cb951f728F24bEB9A5FaaA8512) - ~~ExchangeV3(implementation): 0xCFba78aecfBcc0B4B748fA58c530D4675BB5D32F~~ - ExchangeV3(implementation): 0x4fb117dcd6d09abf1a99b502d488a99f5a17e7ec - LoopringAmmPool(implementation): 0xCAC49516e6E1c79a62BD67E4D87F7E0d80858258 - ### UPDATE-20210310 + - ForcedWithdrawalAgent: 0x52ea1971C05B0169c02a0bBeC05Fe8b5E3A24470 --- @@ -445,6 +447,7 @@ ``` 24. BEL-ETH: 0x567c1ad6d736755abcb3df8ef794b09bb7701e66 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -462,6 +465,7 @@ ``` 25. OBTC-ETH: 0x85f2e9474d208a11ac18ed2a4e434c4bfc6ddbde + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -479,6 +483,7 @@ ``` 26. ENJ-ETH: 0x1b04a25a0a7f93cfb0c4278ca4f7ca2483a1e94e + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -496,6 +501,7 @@ ``` 27. NIOX-ETH: 0x8cf6c5e7ec123583e1529d8afaeaa3d25da2fd3d + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -513,6 +519,7 @@ ``` 28. AMP-ETH: 0x0aa4d2dd35418d63af13ea906ce3a088dec8d786 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -530,6 +537,7 @@ ``` 29. INDEX-ETH: 0x0089081950b4ebbf362689519c1d54827e99d727 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -547,6 +555,7 @@ ``` 30. GRT-ETH: 0x583208883277896435b9821a64806d708de17df2 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -564,6 +573,7 @@ ``` 31. KEEP-ETH: 0x4e585bad734f0c6af04a3afb359fdb69435fe74b + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -581,6 +591,7 @@ ``` 32. DXD-ETH: 0x9387e06961988726dd0732b6930be1c0a5343901 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -598,6 +609,7 @@ ``` 33. ~~TRB-ETH: 0xe8ea36f850db564408e4165a92bccb4e6e5f5e20~~ + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -615,6 +627,7 @@ ``` 34. RPL-ETH: 0x33df027650cd2729e0b132fc0bff4788725cc0fa + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -632,6 +645,7 @@ ``` 35. PBTC-ETH: 0x22844c482b0626ac09b5689b4d8e81fe6710f5f4 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -649,6 +663,7 @@ ``` 36. COMP-ETH: 0x9c601377fd95410be46cfc1a786686874c6e7702 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -666,6 +681,7 @@ ``` 37. PNT-ETH: 0x43eca2f58d8c371c5073fc382784a3a483005d6b + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -683,6 +699,7 @@ ``` 38. PNK-ETH: 0x78a58558ca76cf66b6c4d72231cf6529ed5bef29 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -700,6 +717,7 @@ ``` 39. NEST-ETH: 0xba64cdf65aea36ff4a58dcf288f1a62923555795 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -717,6 +735,7 @@ ``` 40. BTU-ETH: 0x73b7bc4463263194eb9b570948fda12244a5ffa8 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -734,6 +753,7 @@ ``` 41. BZRX-ETH: 0xaced28432cd60d7d34799de0d745871e5f10f961 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -751,6 +771,7 @@ ``` 42. GRID-ETH: 0xa762d8422237bd26b4f882c5d0744726eb2a86b0 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -768,6 +789,7 @@ ``` 43. RENBTC-ETH: 0x636a3141d48402d06a907aa14f023e8f5b5d634f + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -785,6 +807,7 @@ ``` 44. GRG-ETH: 0x37b6aad464e8916dc8231ae5f8aee15dd244c1b1 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -802,6 +825,7 @@ ``` 45. CRV-ETH: 0x2eab3234ea1e4c9571c2e011f435c7316ececdb9 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -819,6 +843,7 @@ ``` 46. BUSD-ETH: 0x8303f865a2a221c920e9fcbf2e84703991f16251 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -836,6 +861,7 @@ ``` 47. BAND-ETH: 0xf11702d591303d790c7b372e53fde348b82037de + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -853,6 +879,7 @@ ``` 48. OGN-ETH: 0x8e89790635dbffdcc0642055cb21abe63edc484c + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -870,6 +897,7 @@ ``` 49. ADX-ETH: 0x1f94eaaa413c11bea645ee65108b5673304753bd + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -887,6 +915,7 @@ ``` 50. PLTC-ETH: 0xfb64c2d72e1caa0286899be8e4f88266c4d8ab9f + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -904,6 +933,7 @@ ``` 51. QCAD-ETH: 0xa738de0f4b1f52cc8410d6e49ab6ed1ca3fe1420 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -921,6 +951,7 @@ ``` 52. FIN-ETH: 0xa0059ad8e06c57458116abc5e5c0bdb86c4fb4b2 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -938,6 +969,7 @@ ``` 53. DOUGH-ETH: 0x24e4cf9b1723e5a5401841d931a301aedecd96ef + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -955,6 +987,7 @@ ``` 54. DEFIL-ETH: 0xa2f4a88553ba746a468c21d3990fe9c503e0b19a + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -972,6 +1005,7 @@ ``` 55. DEFIS-ETH: 0xa2acf6b0304a808147ee3b10601e452c3f1bfde7 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -989,6 +1023,7 @@ ``` 56. AAVE-ETH: 0x8f5a6e6d18f8e3fdffc27fe7fe5804c2378f8310 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1006,6 +1041,7 @@ ``` 57. MKR-ETH: 0x69a8bdee1af2138c58b1261373b37071850689c0 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1023,6 +1059,7 @@ ``` 58. BAL-ETH: 0x145f20a0c129d592da261e42947a70be3b22db07 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1040,6 +1077,7 @@ ``` 59. REN-ETH: 0xee6a9d6cb11a9796f767540f435f90f11a9b1414 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1057,6 +1095,7 @@ ``` 60. UMA-ETH: 0x8a6ba9d448ad54579bed1f42f587d134bf7f8582 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1074,6 +1113,7 @@ ``` 61. OMG-ETH: 0x8a986607603d606b1ac5fdcca089764671c725e1 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1091,6 +1131,7 @@ ``` 62. KNC-ETH: 0xf85f030865359d1843701f4f1b08c38913c3d57f + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1108,6 +1149,7 @@ ``` 63. BNT-ETH: 0x7ab580e6af77bd13f090619ee1f7e7c2a645afb1 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1125,6 +1167,7 @@ ``` 64. SNT-ETH: 0xfe88c469e27861907d05a0e97f81d84c789a1cda + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1142,6 +1185,7 @@ ``` 65. GNO-ETH: 0x447356b190c7dafbe0452c8d041725abf1e1d41f + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1159,6 +1203,7 @@ ``` 66. CVT-ETH: 0x8f871ac37fa7f575e9b8c285b38f0bf99d3c087f + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1176,6 +1221,7 @@ ``` 67. ENTRP-ETH: 0x41e3b439a4798f2f466d28be7bedc0743847dbe4 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1193,6 +1239,7 @@ ``` 68. FARM-ETH: 0x5f6a9960318903d4205dda6ba45796bc969461b8 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1210,6 +1257,7 @@ ``` 69. ZRX-ETH: 0xbbca4790398c4ce916937db3c6b7e9a9da6502e8 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1227,6 +1275,7 @@ ``` 70. NMR-ETH: 0x093137cfd844b64febeb5371d85cf83ff4f92bbf + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1244,6 +1293,7 @@ ``` 71. LRC-WBTC: 0xfa6680779dc9168600bcdcaff28b41c8fa568d98 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1259,7 +1309,9 @@ tokenSymbol: 'LP-LRC-WBTC' } ``` + 72. RFOX-ETH: 0x1ad74cf7caf443f77bd89860ef39f4ca16fbe810 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1277,6 +1329,7 @@ ``` 73. NEC-ETH: 0xc418a3af58d7a1bad0b709fe58d0afddf64e178d + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1294,6 +1347,7 @@ ``` 74. WBTC-USDT: 0xe6f1c20d06b2f541e4308d752d0d58c6df07191d + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1311,6 +1365,7 @@ ``` 75. WBTC-USDC: 0x7af6e5dd61c93277b406ffcadad6e6089b27075b + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1328,6 +1383,7 @@ ``` 76. WBTC-DAI: 0x759c0d0ce4191db16ef5bce6ed0a05de9e99a9f5 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1345,6 +1401,7 @@ ``` 77. RAI-ETH: 0x994f94c853d691f5c775e5131fc4a110abeed4a8 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1362,6 +1419,7 @@ ``` 78. SNX-ETH: 0xe7e807631f3e807ae20d0e23919db8789680104b + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1379,6 +1437,7 @@ ``` 79. RGT-ETH: 0x7cd7871181d91af440dd4552bae70b8ebe9fba73 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1396,6 +1455,7 @@ ``` 80. VSP-ETH: 0x9a94a815f56d00f52bbad46edc6d12d879df2635 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1413,6 +1473,7 @@ ``` 81. SMARTCREDIT-ETH: 0xfd997e572f03f3ff4f117aaccaab9b45bfb6e01c + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1430,6 +1491,7 @@ ``` 82. TEL-ETH: 0x5f24c3a2c9841c023d6646402fd449665b64626b + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1447,6 +1509,7 @@ ``` 83. BCP-ETH: 0x9775449efdf24b7eb5391e7d3758e184595e4c69 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1464,6 +1527,7 @@ ``` 84. BADGER-ETH: 0x4f23ca1cc6253dc1ba69a07a892d68f3b777c407 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1481,6 +1545,7 @@ ``` 85. SUSHI-ETH: 0x5c159d164b8fd7f0599c625988dc2db68df14842 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1498,6 +1563,7 @@ ``` 86. MASK-ETH: 0x8572b8a876f47d70128c73bfca049ce00eb77563 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1515,6 +1581,7 @@ ``` 87. YPIE-ETH: 0xbbb360538b07b59ba2ca1c9f847c8bc760b8f0d7 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1532,6 +1599,7 @@ ``` 88. FUSE-ETH: 0x34841262432975e36755ab797cb523dd7248861a + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1549,6 +1617,7 @@ ``` 89. MASK-LRC: 0xc8f242b2ac6069ebdc876ba0ef42efbf03c5ba4b + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1566,6 +1635,7 @@ ``` 90. SX-ETH: 0xb27b1fd0d4a7d91d07c19f9a33d3a4711a453d7c + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1583,6 +1653,7 @@ ``` 91. REPT-ETH: 0x76d8ea32c511a87ee4bff5f00e758dd362adf3d0 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1600,6 +1671,7 @@ ``` 92. RSPT-USDC: 0x6bf0060fbcf271a2ed828e77076543076d5edba1 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1617,6 +1689,7 @@ ``` 93. RSR-ETH: 0x554be7b23fde679049e52f195448db28b624534e + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1634,6 +1707,7 @@ ``` 94. UBT-ETH: 0xa41e49fdcd0555484f70899d95593d2e1a0fcbbb + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1651,6 +1725,7 @@ ``` 95. BAT-ETH: 0x83df13e357c731ec92d13cbf8f5bf4765a8e1205 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1668,6 +1743,7 @@ ``` 96. 0xBTC-ETH: 0x4facf65a157678e62f84389dd248d99f828403d6 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1685,6 +1761,7 @@ ``` 97. HEX-ETH: 0xc3630669cb660f9405df0d0037f52b78c49772ab + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1702,6 +1779,7 @@ ``` 98. OVR-ETH: 0x7b854d37e502771b1647f5917efcf065ce1c0677 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1719,6 +1797,7 @@ ``` 99. BCDT-ETH: 0x66fAD4Ab701eE8C6F9eBef93b634a3E7401aa276 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1736,6 +1815,7 @@ ``` 100. ALCX-ETH: 0x18a1A6F47Fd92185b91edc322d1954349aD0b652 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1753,6 +1833,7 @@ ``` 101. FLI-ETH: 0x4a7e38476b05F40B16E5ae1C761302B1A7d5afc5 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1770,6 +1851,7 @@ ``` 102. JRT-ETH: 0x83c11cbfbED2971032d3a1eD2f34d4Fb43FE181F + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1787,6 +1869,7 @@ ``` 103. ICHI-ETH: 0xc6bc133562b470a61394f9a2ff7fe8082da698a4 + ``` { sharedConfig: '0x19b28198D993d3B0b1807C7bd46b4F0a4AFD473D', @@ -1819,6 +1902,7 @@ tokenSymbol: 'LP-DPR-ETH' } ``` + *** - Registered tokens: @@ -1848,7 +1932,7 @@ - BUSD: 0x4fabb145d64652a948d72533023f6e7a623c7c53, tokenId: 23 - ~~SNX: 0xc011a72400e58ecd99ee497cf89e3775d4bd732f, tokenId: 24~~ - GNO: 0x6810e776880c02933d47db1b9fc05908e5386b96, tokenId: 25 - - ~~LEND: 0x80fB784B7eD66730e8b1DBd9820aFD29931aab03, tokenId: 26~~ // migrate to AAVE + - ~~LEND: 0x80fB784B7eD66730e8b1DBd9820aFD29931aab03, tokenId: 26~~ // migrate to AAVE - REN: 0x408e41876cccdc0f92210600ef50372656052a38, tokenId: 27 - ~~REP: 0x1985365e9f78359a9B6AD760e32412f4a445E862, tokenId: 28~~ // old contract - BNT: 0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C, tokenId: 29 diff --git a/packages/loopring_v3/ganache.sh b/packages/loopring_v3/ganache.sh index cacac7b9d..a23d4e85c 100755 --- a/packages/loopring_v3/ganache.sh +++ b/packages/loopring_v3/ganache.sh @@ -2,54 +2,54 @@ ganache-cli \ -l 6700000 \ - --account="0x7c71142c72a019568cf848ac7b805d21f2e0fd8bc341e8314580de11c6a397bf,1000000000000000000000"\ - --account="0x4c5496d2745fe9cc2e0aa3e1aad2b66cc792a716decf707ddb3f92bd2d93ad24,1000000000000000000000"\ - --account="0x04b9e9d7c1385c581bab12600834f4f90c6e19142faae6c2de670bfb4b5a08c4,1000000000000000000000"\ - --account="0xa99a8d27d06380565d1cf6c71974e7707a81676c4e7cb3dad2c43babbdca2d23,1000000000000000000000"\ - --account="0x9fda7156489be5244d8edc3b2dafa6976c14c729d54c21fb6fd193fb72c4de0d,1000000000000000000000"\ - --account="0x2949899bb4312754e11537e1e2eba03c0298608effeab21620e02a3ef68ea58a,1000000000000000000000"\ - --account="0x86768554c0bdef3a377d2dd180249936db7010a097d472293ae7808536ea45a9,1000000000000000000000"\ - --account="0x6be54ed053274a3cda0f03aa9f9ddd4cafbb7bd03ceffe8731ed76c0f0be3297,1000000000000000000000"\ - --account="0x05a94ee2777a19a7e1ed0c58d2d61b857bb9cd712168cd16848163f12eb80e45,1000000000000000000000"\ - --account="0x324b720be128e8cacb16395deac8b1332d02da4b2577d4cd94cc453302320ea7,1000000000000000000000"\ - --account="0x25aa7680c43630318fad7ff2aa7ebb6a7aa8d8e599cbbe5b3de25de20dfe4e1b,1000000000000000000000"\ - --account="0x918f1cc0581f423d55454112a034e12902a71a8d5dcdb798a8781b40534db976,1000000000000000000000"\ - --account="0x679e3bef96db80e9e293da28ede5503e95babaf85e6bb5afa4f0363591629d89,1000000000000000000000"\ - --account="0xfeb462cc1a1338c8d2f64eccea5fdff5e8d9900dc78d4577e2db49571b0699b1,1000000000000000000000"\ - --account="0x2cebf2be8c8542bc9ab08f8bfd6e5cbd77b7ce3ba30d99bea19887ef4b24f08c,1000000000000000000000"\ - --account="0x22a6da9181720f347e65a0df66ca8cf57e60321f8e1543321c61cdea586212a6,1000000000000000000000"\ - --account="0x0e41cca4fb0effd4564814ed6c4ba3cccdf47933175574109e247976fa9aead8,1000000000000000000000"\ - --account="0x1c5d1d8cdd8d9abcf0fa60bfbab86be6b33a42053bc2f9b11f1021b52e7f840c,1000000000000000000000"\ - --account="0x80fc4b4b75850d8c0958b341bb8eae1f79819a00902d3744aa02eb8c7b9cb190,1000000000000000000000"\ - --account="0xe650c108f3904da6078339df60b5d5cb325176f0e79080dd6a138cb3d263e1bc,1000000000000000000000"\ - --account="0x85cab09b0ad47c35acd100f664f7ecbc98ad82a1c63e836723d05d277942e912,1000000000000000000000"\ - --account="0x923a8a6b3e00af1ea8668c6842b7ecc028c5d40646189557bd5d2a948a44aaad,1000000000000000000000"\ - --account="0x1fbd4ac17c5eabf5a2d9a27eb659ee5da3cd45de2c798bf81a8bbab92e198236,1000000000000000000000"\ - --account="0xae6243ecefe50a7237f7740213f23aa87bd989f6ed2f3b52a1382949a1858953,1000000000000000000000"\ - --account="0xc1db1e05b3fec89b15809f91fc1a061ad475b50da67df548df3aaaed1002561e,1000000000000000000000"\ - --account="0x9550cc493b2a691d7ebd5f1fcb62a149eb076d4bf22ba57a9bef98c097df97a1,1000000000000000000000"\ - --account="0x925871d77ddcc56f2561201a4c55c4019843291b2eacd4fc9adae96d7b22f5c8,1000000000000000000000"\ - --account="0x72f30ea14204d5f097195dd589fc88054410b3b9ae0eca507f63063f2a9917e1,1000000000000000000000"\ - --account="0x1c2d58c6b1e7e7d6a1138afb4d792ef22f52b2d435fd87ac4962fbca3052cb0e,1000000000000000000000"\ - --account="0x563a0da4dfbe88aef3d343be7524a65648b35bf0607d4ee2c3aedd4f6830d23a,1000000000000000000000"\ - --account="0x516444910fadbb8ac2af5d52acd30c34f3520bf8587f29e5055d39c5e4fcbff3,1000000000000000000000"\ - --account="0xf5f2a3ba2f74c5d895566fbd9445ba0c210b7b8924cb4aed8cc5973c8d0d128b,1000000000000000000000"\ - --account="0x3eddec4001f23f5d029a6f6acbb4b5677d904b2db8ff89ea24c6ae45d6bf9be6,1000000000000000000000"\ - --account="0xf89c65e351038e1298483d4d15b6c818df3805fd6a222bf741ff8ec39a0af92a,1000000000000000000000"\ - --account="0x034d1db40de6d12d604c814fda4da180d0f086671af5eea83f0aa3f66511d21c,1000000000000000000000"\ - --account="0x7040ba2e737ebe9ce2e0bdf0915e6bee7a791dd4e23b55fcb9001c72f4ef7ea2,1000000000000000000000"\ - --account="0xcdfe60b27d0c14475abd2ca3e18afd4ff881bad030e5703cdcb57d74e0bf6f6c,1000000000000000000000"\ - --account="0x024f728fcd2c88d97635bbf4c6f811c8752bb0b438d9d4634d225f9b645a1c4e,1000000000000000000000"\ - --account="0xf7752d03bbc6aa7be10e8cd572041a59b5db892e0740b87139903c60645fe046,1000000000000000000000"\ - --account="0xa429313a6b597efdb47c950b5a2b336cfd2ad5c62b6c6af5e43a007f493c14bc,1000000000000000000000"\ - --account="0x5e84cfc05aee7e0bc2f6c8559f9828fe79ed23bbf9564dc042cdec89f4200748,1000000000000000000000"\ - --account="0x9e0cde2ab01ec05d71d93e491203cb66e5e62f6a55fe7a28198fef4e8e6d89c3,1000000000000000000000"\ - --account="0x845e000ea6c6fbe8f3ba726399faeafd531ac186e5a442091e4bcff0f21db37b,1000000000000000000000"\ - --account="0xee0989a5bcb4fee9dccc5f728b6c1e7dfac5eed73214b741509edd7d0e647fd0,1000000000000000000000"\ - --account="0x0783fd7d502d70894edfe6b519495285edf6ebecee90278056ac04120e596535,1000000000000000000000"\ - --account="0xcd1f81bdb6e47a6b8854e3f54455257cfb06a8d2c8d3cb7338bca7907f937367,1000000000000000000000"\ - --account="0x03f86ff7366dc323672c7d22b9aca83fd6e1a981fabfe7938a8345240772a4fc,1000000000000000000000"\ - --account="0xc89e22bb514b880c77eb04b6355a977e12ab6e24b77bfdc4390d21c5d2296325,1000000000000000000000"\ - --account="0xb6363ec295018ed93759777139049dbb098734843c311ebb9951c1e93feffcb4,1000000000000000000000"\ - --account="0x3c3cb9b2fcab41e588d5aa0066928f855f2cf09e5c817fc41350eae9cfe8dc36,1000000000000000000000"\ + --account="0x7c71142c72a019568cf848ac7b805d21f2e0fd8bc341e8314580de11c6a397bf,10000000000000000000000"\ + --account="0x4c5496d2745fe9cc2e0aa3e1aad2b66cc792a716decf707ddb3f92bd2d93ad24,10000000000000000000000"\ + --account="0x04b9e9d7c1385c581bab12600834f4f90c6e19142faae6c2de670bfb4b5a08c4,10000000000000000000000"\ + --account="0xa99a8d27d06380565d1cf6c71974e7707a81676c4e7cb3dad2c43babbdca2d23,10000000000000000000000"\ + --account="0x9fda7156489be5244d8edc3b2dafa6976c14c729d54c21fb6fd193fb72c4de0d,10000000000000000000000"\ + --account="0x2949899bb4312754e11537e1e2eba03c0298608effeab21620e02a3ef68ea58a,10000000000000000000000"\ + --account="0x86768554c0bdef3a377d2dd180249936db7010a097d472293ae7808536ea45a9,10000000000000000000000"\ + --account="0x6be54ed053274a3cda0f03aa9f9ddd4cafbb7bd03ceffe8731ed76c0f0be3297,10000000000000000000000"\ + --account="0x05a94ee2777a19a7e1ed0c58d2d61b857bb9cd712168cd16848163f12eb80e45,10000000000000000000000"\ + --account="0x324b720be128e8cacb16395deac8b1332d02da4b2577d4cd94cc453302320ea7,10000000000000000000000"\ + --account="0x25aa7680c43630318fad7ff2aa7ebb6a7aa8d8e599cbbe5b3de25de20dfe4e1b,10000000000000000000000"\ + --account="0x918f1cc0581f423d55454112a034e12902a71a8d5dcdb798a8781b40534db976,10000000000000000000000"\ + --account="0x679e3bef96db80e9e293da28ede5503e95babaf85e6bb5afa4f0363591629d89,10000000000000000000000"\ + --account="0xfeb462cc1a1338c8d2f64eccea5fdff5e8d9900dc78d4577e2db49571b0699b1,10000000000000000000000"\ + --account="0x2cebf2be8c8542bc9ab08f8bfd6e5cbd77b7ce3ba30d99bea19887ef4b24f08c,10000000000000000000000"\ + --account="0x22a6da9181720f347e65a0df66ca8cf57e60321f8e1543321c61cdea586212a6,10000000000000000000000"\ + --account="0x0e41cca4fb0effd4564814ed6c4ba3cccdf47933175574109e247976fa9aead8,10000000000000000000000"\ + --account="0x1c5d1d8cdd8d9abcf0fa60bfbab86be6b33a42053bc2f9b11f1021b52e7f840c,10000000000000000000000"\ + --account="0x80fc4b4b75850d8c0958b341bb8eae1f79819a00902d3744aa02eb8c7b9cb190,10000000000000000000000"\ + --account="0xe650c108f3904da6078339df60b5d5cb325176f0e79080dd6a138cb3d263e1bc,10000000000000000000000"\ + --account="0x85cab09b0ad47c35acd100f664f7ecbc98ad82a1c63e836723d05d277942e912,10000000000000000000000"\ + --account="0x923a8a6b3e00af1ea8668c6842b7ecc028c5d40646189557bd5d2a948a44aaad,10000000000000000000000"\ + --account="0x1fbd4ac17c5eabf5a2d9a27eb659ee5da3cd45de2c798bf81a8bbab92e198236,10000000000000000000000"\ + --account="0xae6243ecefe50a7237f7740213f23aa87bd989f6ed2f3b52a1382949a1858953,10000000000000000000000"\ + --account="0xc1db1e05b3fec89b15809f91fc1a061ad475b50da67df548df3aaaed1002561e,10000000000000000000000"\ + --account="0x9550cc493b2a691d7ebd5f1fcb62a149eb076d4bf22ba57a9bef98c097df97a1,10000000000000000000000"\ + --account="0x925871d77ddcc56f2561201a4c55c4019843291b2eacd4fc9adae96d7b22f5c8,10000000000000000000000"\ + --account="0x72f30ea14204d5f097195dd589fc88054410b3b9ae0eca507f63063f2a9917e1,10000000000000000000000"\ + --account="0x1c2d58c6b1e7e7d6a1138afb4d792ef22f52b2d435fd87ac4962fbca3052cb0e,10000000000000000000000"\ + --account="0x563a0da4dfbe88aef3d343be7524a65648b35bf0607d4ee2c3aedd4f6830d23a,10000000000000000000000"\ + --account="0x516444910fadbb8ac2af5d52acd30c34f3520bf8587f29e5055d39c5e4fcbff3,10000000000000000000000"\ + --account="0xf5f2a3ba2f74c5d895566fbd9445ba0c210b7b8924cb4aed8cc5973c8d0d128b,10000000000000000000000"\ + --account="0x3eddec4001f23f5d029a6f6acbb4b5677d904b2db8ff89ea24c6ae45d6bf9be6,10000000000000000000000"\ + --account="0xf89c65e351038e1298483d4d15b6c818df3805fd6a222bf741ff8ec39a0af92a,10000000000000000000000"\ + --account="0x034d1db40de6d12d604c814fda4da180d0f086671af5eea83f0aa3f66511d21c,10000000000000000000000"\ + --account="0x7040ba2e737ebe9ce2e0bdf0915e6bee7a791dd4e23b55fcb9001c72f4ef7ea2,10000000000000000000000"\ + --account="0xcdfe60b27d0c14475abd2ca3e18afd4ff881bad030e5703cdcb57d74e0bf6f6c,10000000000000000000000"\ + --account="0x024f728fcd2c88d97635bbf4c6f811c8752bb0b438d9d4634d225f9b645a1c4e,10000000000000000000000"\ + --account="0xf7752d03bbc6aa7be10e8cd572041a59b5db892e0740b87139903c60645fe046,10000000000000000000000"\ + --account="0xa429313a6b597efdb47c950b5a2b336cfd2ad5c62b6c6af5e43a007f493c14bc,10000000000000000000000"\ + --account="0x5e84cfc05aee7e0bc2f6c8559f9828fe79ed23bbf9564dc042cdec89f4200748,10000000000000000000000"\ + --account="0x9e0cde2ab01ec05d71d93e491203cb66e5e62f6a55fe7a28198fef4e8e6d89c3,10000000000000000000000"\ + --account="0x845e000ea6c6fbe8f3ba726399faeafd531ac186e5a442091e4bcff0f21db37b,10000000000000000000000"\ + --account="0xee0989a5bcb4fee9dccc5f728b6c1e7dfac5eed73214b741509edd7d0e647fd0,10000000000000000000000"\ + --account="0x0783fd7d502d70894edfe6b519495285edf6ebecee90278056ac04120e596535,10000000000000000000000"\ + --account="0xcd1f81bdb6e47a6b8854e3f54455257cfb06a8d2c8d3cb7338bca7907f937367,10000000000000000000000"\ + --account="0x03f86ff7366dc323672c7d22b9aca83fd6e1a981fabfe7938a8345240772a4fc,10000000000000000000000"\ + --account="0xc89e22bb514b880c77eb04b6355a977e12ab6e24b77bfdc4390d21c5d2296325,10000000000000000000000"\ + --account="0xb6363ec295018ed93759777139049dbb098734843c311ebb9951c1e93feffcb4,10000000000000000000000"\ + --account="0x3c3cb9b2fcab41e588d5aa0066928f855f2cf09e5c817fc41350eae9cfe8dc36,10000000000000000000000"\ --acctKeys="ganache_account_keys.txt" diff --git a/packages/loopring_v3/test/ammUtils.ts b/packages/loopring_v3/test/ammUtils.ts index 5e8fbbe99..8ccc17109 100644 --- a/packages/loopring_v3/test/ammUtils.ts +++ b/packages/loopring_v3/test/ammUtils.ts @@ -1,7 +1,7 @@ import BN = require("bn.js"); import { Constants, Signature } from "loopringV3.js"; import { ExchangeTestUtil } from "./testExchangeUtil"; -import { AuthMethod, BlockCallback } from "./types"; +import { AuthMethod, TransactionReceiverCallback } from "./types"; import * as sigUtil from "eth-sig-util"; import { SignatureType, sign, verifySignature } from "../util/Signature"; import { roundToFloatValue } from "loopringV3.js"; @@ -729,15 +729,15 @@ export class AmmPool { ]); } - public static getBlockCallback(transaction: TxType) { - const blockCallback: BlockCallback = { + public static getTransactionReceiverCallback(transaction: TxType) { + const transactionReceiverCallback: TransactionReceiverCallback = { target: transaction.poolAddress, txIdx: transaction.txIdx, numTxs: transaction.numTxs, auxiliaryData: AmmPool.getAuxiliaryData(transaction), tx: transaction }; - return blockCallback; + return transactionReceiverCallback; } public async verifySupply(expectedTotalSupply?: BN) { diff --git a/packages/loopring_v3/test/testBridge.ts b/packages/loopring_v3/test/testBridge.ts new file mode 100644 index 000000000..171af250c --- /dev/null +++ b/packages/loopring_v3/test/testBridge.ts @@ -0,0 +1,1316 @@ +import BN = require("bn.js"); +import { AmmPool, Permit, PermitUtils } from "./ammUtils"; +import { expectThrow } from "./expectThrow"; +import { Constants, roundToFloatValue } from "loopringV3.js"; +import { + BalanceSnapshot, + ExchangeTestUtil, + TransferUtils +} from "./testExchangeUtil"; +import { AuthMethod, Transfer } from "./types"; +import { SignatureType, sign, verifySignature } from "../util/Signature"; +import * as sigUtil from "eth-sig-util"; +import { Bitstream } from "loopringV3.js"; + +const AgentRegistry = artifacts.require("AgentRegistry"); + +const BridgeContract = artifacts.require("Bridge"); +const TestSwapper = artifacts.require("TestSwapper"); +const TestSwappperBridgeConnector = artifacts.require( + "TestSwappperBridgeConnector" +); +const TestMigrationBridgeConnector = artifacts.require( + "TestMigrationBridgeConnector" +); + +export interface BridgeTransfer { + owner: string; + token: string; + amount: string; +} + +export interface InternalBridgeTransfer { + owner: string; + tokenID: number; + amount: string; +} + +export interface TokenData { + token: string; + tokenID: number; + amount: string; +} + +export interface BridgeCall { + owner: string; + token: string; + amount: string; + feeToken: string; + userData: string; + minGas: number; + maxFee: string; + validUntil: number; + connector: string; + groupData: string; + expectedDeposit?: BridgeTransfer; +} + +export interface ConnectorGroup { + groupData: string; + calls: BridgeCall[]; +} + +export interface ConnectorCalls { + connector: string; + gasLimit: number; + groups: ConnectorGroup[]; + totalMinGas: number; + tokens: TokenData[]; +} + +export interface TransferBatch { + batchID: number; + amounts: string[]; +} + +export interface BridgeOperations { + transferBatches: TransferBatch[]; + connectorCalls: ConnectorCalls[]; + tokens: TokenData[]; +} + +export interface BridgeCallWrapper { + transfer: Transfer; + connector: string; + groupData: string; + call: BridgeCall; +} + +export namespace CollectTransferUtils { + export function toTypedData( + callWrapper: BridgeCallWrapper, + verifyingContract: string + ) { + const typedData = { + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" } + ], + BridgeCall: [ + { name: "tokenID", type: "uint16" }, + { name: "amount", type: "uint96" }, + { name: "feeTokenID", type: "uint16" }, + { name: "maxFee", type: "uint96" }, + { name: "validUntil", type: "uint32" }, + { name: "storageID", type: "uint32" }, + { name: "minGas", type: "uint32" }, + { name: "connector", type: "address" }, + { name: "groupData", type: "bytes" }, + { name: "userData", type: "bytes" } + ] + }, + primaryType: "BridgeCall", + domain: { + name: "Bridge", + version: "1.0", + chainId: new BN(/*await web3.eth.net.getId()*/ 1), + verifyingContract + }, + message: { + tokenID: callWrapper.transfer.tokenID, + amount: callWrapper.transfer.amount, + feeTokenID: callWrapper.transfer.feeTokenID, + maxFee: callWrapper.call.maxFee, + validUntil: callWrapper.call.validUntil, + storageID: callWrapper.transfer.storageID, + minGas: callWrapper.call.minGas, + connector: callWrapper.connector, + groupData: callWrapper.groupData, + userData: callWrapper.call.userData + } + }; + return typedData; + } + + export function getHash( + callWrapper: BridgeCallWrapper, + verifyingContract: string + ) { + const typedData = this.toTypedData(callWrapper, verifyingContract); + return sigUtil.TypedDataUtils.sign(typedData); + } +} + +export class Bridge { + public ctx: ExchangeTestUtil; + public contract: any; + public address: string; + + public accountID: number; + + public relayer: string; + + public migrationConnector: string; + + constructor(ctx: ExchangeTestUtil) { + this.ctx = ctx; + this.relayer = ctx.testContext.orderOwners[11]; + } + + public async setup() { + this.accountID = this.ctx.accounts[this.ctx.exchangeId].length; + + this.contract = await BridgeContract.new( + this.ctx.exchange.address, + this.accountID + ); + + // Create the Bridge account + const owner = this.contract.address; + const deposit = await this.ctx.deposit( + this.ctx.testContext.orderOwners[0], + owner, + "ETH", + new BN(1), + { autoSetKeys: false } + ); + assert(deposit.accountID === this.accountID, "unexpected accountID"); + + //console.log(this.contract); + //console.log(this.contract.contract); + //console.log(this.contract.contract.methods); + + this.address = this.contract.address; + } + + public async setMigrationConnectorAddress(migrationConnector: string) { + this.migrationConnector = migrationConnector; + } + + public async batchDeposit(deposits: BridgeTransfer[]) { + const tokens: Map = new Map(); + for (const deposit of deposits) { + if (!tokens.has(deposit.token)) { + tokens.set(deposit.token, new BN(0)); + } + tokens.set( + deposit.token, + tokens.get(deposit.token).add(new BN(deposit.amount)) + ); + } + + let ethValue = new BN(0); + for (const [token, amount] of tokens.entries()) { + if (token === Constants.zeroAddress) { + ethValue = tokens.get(Constants.zeroAddress); + } else { + await this.ctx.setBalanceAndApprove(this.relayer, token, amount); + } + } + + const tx = await this.contract.batchDeposit(deposits, { + from: this.relayer, + value: ethValue + }); + console.log( + "\x1b[46m%s\x1b[0m", + "[BatchDeposit] Gas used: " + tx.receipt.gasUsed + ); + const transferEvents = await this.ctx.getEvents(this.contract, "Transfers"); + + const depositEvents = await this.ctx.assertEventsEmitted( + this.ctx.exchange, + "DepositRequested", + tokens.size + ); + + // Process the deposits + for (const [token, amount] of tokens.entries()) { + await this.ctx.requestDeposit(this.address, token, amount); + } + + await this.ctx.submitTransactions(); + await this.ctx.submitPendingBlocks(); + + return transferEvents; + } + + public async setupCalls(calls: BridgeCall[]) { + for (const call of calls) { + await this.ctx.deposit( + call.owner, + call.owner, + call.token, + new BN(call.amount) + ); + call.token = await this.ctx.getTokenAddress(call.token); + } + + await this.ctx.submitTransactions(); + await this.ctx.submitPendingBlocks(); + } + + public decodeTransfers(_data: string) { + const transfers: InternalBridgeTransfer[] = []; + const data = new Bitstream(_data); + for (let i = 0; i < data.length() / 34; i++) { + const transfer: InternalBridgeTransfer = { + owner: data.extractAddress(i * 34 + 0), + tokenID: data.extractUint16(i * 34 + 32), + amount: data.extractUint96(i * 34 + 20).toString(10) + }; + transfers.push(transfer); + } + return transfers; + } + + public async submitBridgeOperations( + transferEvents: any[], + calls: BridgeCall[], + expectedSuccess?: boolean[], + changeTransfers?: boolean + ) { + changeTransfers = changeTransfers ? true : false; + console.log("Change transfers: " + changeTransfers); + + const bridgeOperations: BridgeOperations = { + transferBatches: [], + connectorCalls: [], + tokens: [] + }; + + const blockCallback = this.ctx.addBlockCallback(this.address); + + for (const event of transferEvents) { + const amounts: string[] = []; + const transfers = this.decodeTransfers(event.transfers); + for (let i = 0; i < transfers.length; i++) { + const transfer = transfers[i]; + transfer.amount = changeTransfers + ? new BN(transfer.amount).div(new BN(2)).toString(10) + : transfer.amount; + await this.ctx.transfer( + this.address, + transfer.owner, + this.ctx.getTokenAddressFromID(transfer.tokenID), + new BN(transfer.amount), + this.ctx.getTokenAddressFromID(transfer.tokenID), + new BN(0), + { + authMethod: AuthMethod.NONE, + amountToDeposit: new BN(0), + feeToDeposit: new BN(0), + transferToNew: true + } + ); + amounts.push(transfer.amount); + } + bridgeOperations.transferBatches.push({ + batchID: event.batchID.toNumber(), + amounts + }); + } + + const tokenMap: Map = new Map(); + for (const call of calls) { + if (!tokenMap.has(call.token)) { + tokenMap.set(call.token, new BN(0)); + } + tokenMap.set( + call.token, + tokenMap.get(call.token).add(new BN(call.amount)) + ); + } + + for (const [token, amount] of tokenMap.entries()) { + bridgeOperations.tokens.push({ + token: token, + tokenID: await this.ctx.getTokenID(token), + amount: amount.toString(10) + }); + } + + // Sort the calls on connector and group + for (const call of calls) { + let connectorCalls: ConnectorCalls; + for (let c = 0; c < bridgeOperations.connectorCalls.length; c++) { + if (bridgeOperations.connectorCalls[c].connector === call.connector) { + connectorCalls = bridgeOperations.connectorCalls[c]; + break; + } + } + if (connectorCalls === undefined) { + const connectorTokens: TokenData[] = []; + for (const tokenData of bridgeOperations.tokens) { + connectorTokens.push({ + token: tokenData.token, + tokenID: tokenData.tokenID, + amount: "0" + }); + } + connectorCalls = { + connector: call.connector, + gasLimit: 2000000, + totalMinGas: 0, + groups: [], + tokens: connectorTokens + }; + bridgeOperations.connectorCalls.push(connectorCalls); + } + + let group: ConnectorGroup; + for (let g = 0; g < connectorCalls.groups.length; g++) { + if (connectorCalls.groups[g].groupData === call.groupData) { + group = connectorCalls.groups[g]; + break; + } + } + if (group === undefined) { + group = { + groupData: call.groupData, + calls: [] + }; + connectorCalls.groups.push(group); + } + group.calls.push(call); + + let tokenData: TokenData; + for (let t = 0; t < connectorCalls.tokens.length; t++) { + if (connectorCalls.tokens[t].token === call.token) { + tokenData = connectorCalls.tokens[t]; + break; + } + } + assert(tokenData !== undefined, "invalid state"); + tokenData.amount = new BN(tokenData.amount) + .add(new BN(call.amount)) + .toString(10); + + connectorCalls.totalMinGas += call.minGas; + } + + // + // Do L2 transactions + // + + for (const connectorCalls of bridgeOperations.connectorCalls) { + for (const group of connectorCalls.groups) { + for (const call of group.calls) { + const transfer = await this.ctx.transfer( + call.owner, + this.address, + call.token, + new BN(call.amount), + call.feeToken, + new BN(0), + { + authMethod: AuthMethod.NONE, + amountToDeposit: new BN(0), + feeToDeposit: new BN(0) + } + ); + + const bridgeCallWrapper: BridgeCallWrapper = { + transfer, + call, + connector: connectorCalls.connector, + groupData: group.groupData + }; + const txHash = CollectTransferUtils.getHash( + bridgeCallWrapper, + this.address + ); + await this.ctx.requestSignatureVerification( + call.owner, + this.ctx.hashToFieldElement("0x" + txHash.toString("hex")) + ); + } + } + } + + for (const token of bridgeOperations.tokens) { + await this.ctx.requestWithdrawal( + this.address, + token.token, + new BN(token.amount), + token.token, + new BN(0), + { + authMethod: AuthMethod.NONE + } + ); + } + + //console.log(bridgeOperations); + + // Set the pool transaction data on the callback + blockCallback.auxiliaryData = this.encodeBridgeOperations(bridgeOperations); + blockCallback.numTxs = calls.length * 2 + bridgeOperations.tokens.length; + for (const batch of bridgeOperations.transferBatches) { + blockCallback.numTxs += batch.amounts.length; + } + + //console.log("Bridge Data:"); + //console.log(blockCallback.auxiliaryData); + + await this.ctx.submitTransactions(); + await this.ctx.submitPendingBlocks(); + + const connectorCallResultEvents = await this.ctx.assertEventsEmitted( + this.contract, + "ConnectorCallResult", + bridgeOperations.connectorCalls.length + ); + + if (expectedSuccess === undefined) { + expectedSuccess = new Array(bridgeOperations.connectorCalls.length).fill( + true + ); + } + + for (let i = 0; i < connectorCallResultEvents.length; i++) { + assert( + bridgeOperations.connectorCalls[i].connector === + connectorCallResultEvents[i].connector, + "unexpected success" + ); + assert( + expectedSuccess[i] === connectorCallResultEvents[i].success, + "unexpected success" + ); + } + + const expectedDepositTransfers: BridgeTransfer[] = []; + const expectedMigrationTransfers: BridgeTransfer[] = []; + for (const calls of bridgeOperations.connectorCalls) { + for (const group of calls.groups) { + for (const call of group.calls) { + if (call.expectedDeposit) { + if (calls.connector === this.migrationConnector) { + expectedMigrationTransfers.push(call.expectedDeposit); + } else { + expectedDepositTransfers.push(call.expectedDeposit); + } + } + } + } + } + + const newTransferEvents = await this.ctx.getEvents( + this.contract, + "Transfers" + ); + if ( + expectedDepositTransfers.length + expectedMigrationTransfers.length > + 0 + ) { + assert.equal( + newTransferEvents.length, + expectedMigrationTransfers.length > 0 ? 2 : 1, + "unexpected number of transfer events" + ); + + for (let c = 0; c < newTransferEvents.length; c++) { + const transfers = this.decodeTransfers(newTransferEvents[c].transfers); + const expectedTransfers = + newTransferEvents.length > 1 && c == 0 + ? expectedMigrationTransfers + : expectedDepositTransfers; + + assert.equal( + transfers.length, + expectedTransfers.length, + "unexpected number of new transfers" + ); + for (let i = 0; i < transfers.length; i++) { + assert.equal( + transfers[i].owner.toLowerCase(), + expectedTransfers[i].owner.toLowerCase(), + "unexpected owner" + ); + assert.equal( + this.ctx.getTokenAddressFromID(transfers[i].tokenID), + this.ctx.getTokenAddress(expectedTransfers[i].token), + "unexpected token" + ); + assert.equal( + transfers[i].amount, + expectedTransfers[i].amount, + "unexpected amount" + ); + } + } + } else { + assert.equal( + newTransferEvents.length, + 0, + "unexpected number of transfer events" + ); + } + } + + public encodeBridgeOperations(bridgeOperations: BridgeOperations) { + //console.log(bridgeOperations); + + const data = this.contract.contract.methods + .encode(bridgeOperations) + .encodeABI(); + + //console.log(data); + + return "0x" + data.slice(2 + (4 + 0) * 2); + + /*const encodedDeposits = web3.eth.abi.encodeParameter( + { + "struct BridgeTransfer[]": { + owner: "address", + token: "address", + amount: "uint96" + } + }, + bridgeOperations.transfers + );*/ + + /*const encodedBridgeOperations = web3.eth.abi.encodeParameter( + { + "BridgeConfig": { + "BridgeTransfer[]": { + owner: "address", + token: "address", + amount: "uint96" + }, + "struct ConnectorCalls[]": { + connector: "address", + "struct ConnectorGroup[]": { + groupData: "bytes", + "struct BridgeCall[]": { + owner: "address", + token: "address", + amount: "uint256", + minGas: "uint256", + maxFee: "uint256", + userData: "bytes" + } + }, + "struct TokenData[]": { + token: "address", + tokenID: "uint16", + amount: "uint256" + } + }, + "struct TokenData[]": { + token: "address", + tokenID: "uint16", + amount: "uint256" + } + } + }, + { + "BridgeTransfer[]": bridgeOperations.transfers + } + ); + return encodedBridgeOperations;*/ + } +} + +contract("Bridge", (accounts: string[]) => { + let ctx: ExchangeTestUtil; + + let agentRegistry: any; + let registryOwner: string; + + let swapper: any; + let swappperBridgeConnectorA: any; + let swappperBridgeConnectorB: any; + + let failingSwapper: any; + let failingSwappperBridgeConnector: any; + + let migrationBridgeConnector: any; + + let rate: BN = new BN(web3.utils.toWei("1", "ether")); + + let ownerA: string; + let ownerB: string; + let ownerC: string; + let ownerD: string; + + const setupBridge = async () => { + const bridge = new Bridge(ctx); + await bridge.setup(); + + await agentRegistry.registerUniversalAgent(bridge.address, true, { + from: registryOwner + }); + + swapper = await TestSwapper.new(rate, false); + + // Add some funds to the swapper contract + for (const token of ["LRC", "WETH", "ETH"]) { + await ctx.transferBalance( + swapper.address, + token, + new BN(web3.utils.toWei("100", "ether")) + ); + } + + swappperBridgeConnectorA = await TestSwappperBridgeConnector.new( + swapper.address + ); + swappperBridgeConnectorB = await TestSwappperBridgeConnector.new( + swapper.address + ); + + failingSwapper = await TestSwapper.new(rate, true); + + failingSwappperBridgeConnector = await TestSwappperBridgeConnector.new( + failingSwapper.address + ); + + migrationBridgeConnector = await TestMigrationBridgeConnector.new( + ctx.exchange.address, + bridge.address + ); + + bridge.setMigrationConnectorAddress(migrationBridgeConnector.address); + + return bridge; + }; + + const encodeSwapGroupSettings = (tokenIn: string, tokenOut: string) => { + return web3.eth.abi.encodeParameter( + { + "struct GroupSettings": { + tokenIn: "address", + tokenOut: "address" + } + }, + { + tokenIn: ctx.getTokenAddress(tokenIn), + tokenOut: ctx.getTokenAddress(tokenOut) + } + ); + }; + + const encodeSwapUserSettings = (minAmountOut: BN) => { + return web3.eth.abi.encodeParameter( + { + "struct UserSettings": { + minAmountOut: "uint" + } + }, + { + minAmountOut: minAmountOut.toString(10) + } + ); + }; + + const encodeMigrateGroupSettings = (token: string) => { + return web3.eth.abi.encodeParameter( + { + "struct GroupSettings": { + token: "address" + } + }, + { + token: ctx.getTokenAddress(token) + } + ); + }; + + const encodeMigrateUserSettings = (to: string) => { + return web3.eth.abi.encodeParameter( + { + "struct UserSettings": { + to: "address" + } + }, + { + to: to + } + ); + }; + + const round = (value: string) => { + return roundToFloatValue(new BN(value), Constants.Float24Encoding).toString( + 10 + ); + }; + + const convert = (amount: string) => { + const RATE_BASE = new BN(web3.utils.toWei("1", "ether")); + return new BN(amount) + .mul(rate) + .div(RATE_BASE) + .toString(10); + }; + + const withdrawFromPendingBatchDepositChecked = async ( + bridge: Bridge, + depositID: number, + transfers: InternalBridgeTransfer[], + indices: number[] + ) => { + // Simulate all transfers + const snapshot = new BalanceSnapshot(ctx); + + // Simulate withdrawals + for (const idx of indices) { + await snapshot.transfer( + bridge.address, + transfers[idx].owner, + ctx.getTokenAddressFromID(transfers[idx].tokenID), + new BN(transfers[idx].amount), + "bridge", + "owner" + ); + } + + // Do the withdrawal + await bridge.contract.withdrawFromPendingBatchDeposit( + depositID, + transfers, + indices + ); + + // Verify balances + await snapshot.verifyBalances(); + }; + + before(async () => { + ctx = new ExchangeTestUtil(); + await ctx.initialize(accounts); + + ctx.blockSizes.push(...[24, 32, 40, 48]); + + ownerA = ctx.testContext.orderOwners[12]; + ownerB = ctx.testContext.orderOwners[13]; + ownerC = ctx.testContext.orderOwners[14]; + ownerD = ctx.testContext.orderOwners[15]; + }); + + after(async () => { + await ctx.stop(); + }); + + beforeEach(async () => { + // Fresh Exchange for each test + await ctx.createExchange(ctx.testContext.stateOwners[0], { + setupTestState: true, + deterministic: true + }); + + // Create the agent registry + registryOwner = accounts[7]; + agentRegistry = await AgentRegistry.new({ from: registryOwner }); + + // Register it on the exchange contract + const wrapper = await ctx.contracts.ExchangeV3.at(ctx.operator.address); + await wrapper.setAgentRegistry(agentRegistry.address, { + from: ctx.exchangeOwner + }); + }); + + describe("Bridge", function() { + this.timeout(0); + + it("Batch deposit", async () => { + const bridge = await setupBridge(); + + const depositsA: BridgeTransfer[] = []; + depositsA.push({ + owner: ownerA, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("1", "ether") + }); + depositsA.push({ + owner: ownerB, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("2.1265", "ether") + }); + depositsA.push({ + owner: ownerB, + token: ctx.getTokenAddress("LRC"), + amount: web3.utils.toWei("26.2154454177", "ether") + }); + depositsA.push({ + owner: ownerA, + token: ctx.getTokenAddress("LRC"), + amount: web3.utils.toWei("1028.2154454177", "ether") + }); + depositsA.push({ + owner: ownerB, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("1.15484511245", "ether") + }); + depositsA.push({ + owner: ownerB, + token: ctx.getTokenAddress("LRC"), + amount: web3.utils.toWei("12545.15484511245", "ether") + }); + + const depositsB: BridgeTransfer[] = []; + depositsB.push({ + owner: ownerB, + token: ctx.getTokenAddress("WETH"), + amount: web3.utils.toWei("12.15484511245", "ether") + }); + depositsB.push({ + owner: ownerB, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("1.15484511245", "ether") + }); + depositsB.push({ + owner: ownerB, + token: ctx.getTokenAddress("LRC"), + amount: web3.utils.toWei("12545.15484511245", "ether") + }); + depositsB.push({ + owner: ownerB, + token: ctx.getTokenAddress("WETH"), + amount: web3.utils.toWei("12.15484511245", "ether") + }); + + const transferEventsA = await bridge.batchDeposit(depositsA); + const transferEventsB = await bridge.batchDeposit(depositsB); + await bridge.submitBridgeOperations( + [...transferEventsA, ...transferEventsB], + [] + ); + + const depositsC: BridgeTransfer[] = []; + depositsC.push({ + owner: ownerB, + token: ctx.getTokenAddress("WETH"), + amount: web3.utils.toWei("1", "ether") + }); + depositsC.push({ + owner: ownerB, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("2", "ether") + }); + depositsC.push({ + owner: ownerB, + token: ctx.getTokenAddress("LRC"), + amount: web3.utils.toWei("3", "ether") + }); + const transferEventsC = await bridge.batchDeposit(depositsC); + // Try to different transfers + await expectThrow( + bridge.submitBridgeOperations(transferEventsC, [], undefined, true), + "UNKNOWN_TRANSFERS" + ); + }); + + it("Bridge calls", async () => { + const bridge = await setupBridge(); + + await bridge.contract.setConnectorTrusted( + swappperBridgeConnectorA.address, + true + ); + await bridge.contract.setConnectorTrusted( + swappperBridgeConnectorB.address, + true + ); + await bridge.contract.setConnectorTrusted( + failingSwappperBridgeConnector.address, + true + ); + await bridge.contract.setConnectorTrusted( + migrationBridgeConnector.address, + true + ); + + const group_ETH_LRC = encodeSwapGroupSettings("ETH", "LRC"); + const group_LRC_ETH = encodeSwapGroupSettings("LRC", "ETH"); + const group_WETH_LRC = encodeSwapGroupSettings("WETH", "LRC"); + + const calls: BridgeCall[] = []; + // Successful swap connector call + // ETH -> LRC + calls.push({ + owner: ownerA, + token: "ETH", + amount: round(web3.utils.toWei("1.0132", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: "0x", + validUntil: 0, + connector: swappperBridgeConnectorA.address, + groupData: group_ETH_LRC, + expectedDeposit: { + owner: ownerA, + token: "LRC", + amount: convert(round(web3.utils.toWei("1.0132", "ether"))) + } + }); + calls.push({ + owner: ownerB, + token: "ETH", + amount: round(web3.utils.toWei("2.0456546565", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: encodeSwapUserSettings( + new BN(web3.utils.toWei("2", "ether")) + ), + validUntil: 0, + connector: swappperBridgeConnectorA.address, + groupData: group_ETH_LRC, + expectedDeposit: { + owner: ownerB, + token: "LRC", + amount: convert(round(web3.utils.toWei("2.0456546565", "ether"))) + } + }); + calls.push({ + owner: ownerC, + token: "ETH", + amount: round(web3.utils.toWei("3.458415454541", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: encodeSwapUserSettings( + new BN(web3.utils.toWei("3.5", "ether")) + ), + validUntil: 0, + connector: swappperBridgeConnectorA.address, + groupData: group_ETH_LRC, + expectedDeposit: { + owner: ownerC, + token: "ETH", + amount: convert(round(web3.utils.toWei("3.458415454541", "ether"))) + } + }); + // WETH -> LRC + calls.push({ + owner: ownerC, + token: "WETH", + amount: round(web3.utils.toWei("6.458415454541", "ether")), + feeToken: "LRC", + maxFee: "0", + minGas: 30000, + userData: encodeSwapUserSettings( + new BN(web3.utils.toWei("3.5", "ether")) + ), + validUntil: 0, + connector: swappperBridgeConnectorA.address, + groupData: group_WETH_LRC, + expectedDeposit: { + owner: ownerC, + token: "LRC", + amount: convert(round(web3.utils.toWei("6.458415454541", "ether"))) + } + }); + + // Different swapper + calls.push({ + owner: ownerD, + token: "ETH", + amount: round(web3.utils.toWei("1.458415454541", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: encodeSwapUserSettings( + new BN(web3.utils.toWei("1", "ether")) + ), + validUntil: 0, + connector: swappperBridgeConnectorB.address, + groupData: group_ETH_LRC, + expectedDeposit: { + owner: ownerD, + token: "LRC", + amount: convert(round(web3.utils.toWei("1.458415454541", "ether"))) + } + }); + calls.push({ + owner: ownerA, + token: "ETH", + amount: round(web3.utils.toWei("1.0132", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: "0x", + validUntil: 0, + connector: swappperBridgeConnectorB.address, + groupData: group_ETH_LRC, + expectedDeposit: { + owner: ownerA, + token: "LRC", + amount: convert(round(web3.utils.toWei("1.0132", "ether"))) + } + }); + + // Unsuccessful swap connector call + calls.push({ + owner: ownerA, + token: "ETH", + amount: round(web3.utils.toWei("1.0132", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: "0x", + validUntil: 0, + connector: failingSwappperBridgeConnector.address, + groupData: group_ETH_LRC, + expectedDeposit: { + owner: ownerA, + token: "ETH", + amount: round(web3.utils.toWei("1.0132", "ether")) + } + }); + calls.push({ + owner: ownerB, + token: "ETH", + amount: round(web3.utils.toWei("2.0456546565", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: encodeSwapUserSettings( + new BN(web3.utils.toWei("2", "ether")) + ), + validUntil: 0, + connector: failingSwappperBridgeConnector.address, + groupData: group_ETH_LRC, + expectedDeposit: { + owner: ownerB, + token: "ETH", + amount: round(web3.utils.toWei("2.0456546565", "ether")) + } + }); + calls.push({ + owner: ownerB, + token: "LRC", + amount: round(web3.utils.toWei("12.458415454541", "ether")), + feeToken: "LRC", + maxFee: "0", + minGas: 30000, + userData: encodeSwapUserSettings( + new BN(web3.utils.toWei("1", "ether")) + ), + validUntil: 0xffffffff, + connector: failingSwappperBridgeConnector.address, + groupData: group_LRC_ETH, + expectedDeposit: { + owner: ownerB, + token: "LRC", + amount: round(web3.utils.toWei("12.458415454541", "ether")) + } + }); + calls.push({ + owner: ownerC, + token: "WETH", + amount: round(web3.utils.toWei("12.458415454541", "ether")), + feeToken: "WETH", + maxFee: "0", + minGas: 30000, + userData: encodeSwapUserSettings( + new BN(web3.utils.toWei("1", "ether")) + ), + validUntil: 0xffffffff, + connector: failingSwappperBridgeConnector.address, + groupData: group_WETH_LRC, + expectedDeposit: { + owner: ownerC, + token: "WETH", + amount: round(web3.utils.toWei("12.458415454541", "ether")) + } + }); + + // Migrate + calls.push({ + owner: ownerA, + token: "ETH", + amount: round(web3.utils.toWei("1.0132", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: "0x", + validUntil: 0, + connector: migrationBridgeConnector.address, + groupData: encodeMigrateGroupSettings("ETH"), + expectedDeposit: { + owner: ownerA, + token: "ETH", + amount: round(web3.utils.toWei("1.0132", "ether")) + } + }); + calls.push({ + owner: ownerB, + token: "ETH", + amount: round(web3.utils.toWei("10.132", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: encodeMigrateUserSettings(ownerA), + validUntil: 0, + connector: migrationBridgeConnector.address, + groupData: encodeMigrateGroupSettings("ETH"), + expectedDeposit: { + owner: ownerA, + token: "ETH", + amount: round(web3.utils.toWei("10.132", "ether")) + } + }); + calls.push({ + owner: ownerB, + token: "LRC", + amount: round(web3.utils.toWei("123.3132", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: encodeMigrateUserSettings(ownerB), + validUntil: 0, + connector: migrationBridgeConnector.address, + groupData: encodeMigrateGroupSettings("LRC"), + expectedDeposit: { + owner: ownerB, + token: "LRC", + amount: round(web3.utils.toWei("123.3132", "ether")) + } + }); + calls.push({ + owner: ownerB, + token: "LRC", + amount: round(web3.utils.toWei("1234.1132", "ether")), + feeToken: "ETH", + maxFee: "0", + minGas: 30000, + userData: encodeMigrateUserSettings(ownerD), + validUntil: 0, + connector: migrationBridgeConnector.address, + groupData: encodeMigrateGroupSettings("LRC"), + expectedDeposit: { + owner: ownerD, + token: "LRC", + amount: round(web3.utils.toWei("1234.1132", "ether")) + } + }); + + await bridge.setupCalls(calls); + await bridge.submitBridgeOperations([], calls, [true, true, false, true]); + + // Handle resulting batched deposits + const depositEvents = await ctx.getEvents( + ctx.exchange, + "DepositRequested" + ); + for (const deposit of depositEvents) { + await ctx.requestDeposit( + bridge.address, + deposit.token, + new BN(deposit.amount) + ); + } + const transferEvents = await ctx.getEvents(bridge.contract, "Transfers"); + await bridge.submitBridgeOperations(transferEvents, []); + + // assert(false); + }); + + it("Manual withdrawal", async () => { + const bridge = await setupBridge(); + + const deposits: BridgeTransfer[] = []; + deposits.push({ + owner: ownerA, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("1", "ether") + }); + deposits.push({ + owner: ownerB, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("1", "ether") + }); + deposits.push({ + owner: ownerB, + token: ctx.getTokenAddress("LRC"), + amount: web3.utils.toWei("1", "ether") + }); + deposits.push({ + owner: ownerA, + token: ctx.getTokenAddress("LRC"), + amount: web3.utils.toWei("1", "ether") + }); + deposits.push({ + owner: ownerB, + token: ctx.getTokenAddress("ETH"), + amount: web3.utils.toWei("1", "ether") + }); + deposits.push({ + owner: ownerB, + token: ctx.getTokenAddress("WETH"), + amount: web3.utils.toWei("1", "ether") + }); + + const transferEvents = await bridge.batchDeposit(deposits); + + await ctx.submitTransactions(); + await ctx.submitPendingBlocks(); + + const withdrawalFee = await ctx.loopringV3.forcedWithdrawalFee(); + await bridge.contract.forceWithdraw( + [ctx.getTokenAddress("ETH"), ctx.getTokenAddress("LRC")], + { + value: withdrawalFee.mul(new BN(2)) + } + ); + + await ctx.requestWithdrawal( + bridge.address, + "ETH", + new BN(web3.utils.toWei("3", "ether")), + "ETH", + new BN(0), + { + authMethod: AuthMethod.FORCE, + skipForcedAuthentication: true + } + ); + + await ctx.requestWithdrawal( + bridge.address, + "LRC", + new BN(web3.utils.toWei("2", "ether")), + "ETH", + new BN(0), + { + authMethod: AuthMethod.FORCE, + skipForcedAuthentication: true + } + ); + + await ctx.submitTransactions(); + await ctx.submitPendingBlocks(); + + const transfers = bridge.decodeTransfers(transferEvents[0].transfers); + + await expectThrow( + bridge.contract.withdrawFromPendingBatchDeposit(0, transfers, [1]), + "TRANSFERS_NOT_TOO_OLD" + ); + + const MAX_AGE_PENDING_TRANSFER = ( + await bridge.contract.MAX_AGE_PENDING_TRANSFER() + ).toNumber(); + await ctx.advanceBlockTimestamp(MAX_AGE_PENDING_TRANSFER + 1); + + await withdrawFromPendingBatchDepositChecked(bridge, 0, transfers, [ + 1, + 3 + ]); + + await withdrawFromPendingBatchDepositChecked(bridge, 0, transfers, [0]); + + await expectThrow( + bridge.contract.withdrawFromPendingBatchDeposit(0, transfers, [1, 2]), + "ALREADY_WITHDRAWN" + ); + }); + }); +}); diff --git a/packages/loopring_v3/test/testConverter.ts b/packages/loopring_v3/test/testConverter.ts new file mode 100644 index 000000000..dc718768c --- /dev/null +++ b/packages/loopring_v3/test/testConverter.ts @@ -0,0 +1,418 @@ +import BN = require("bn.js"); +import { AmmPool, Permit, PermitUtils } from "./ammUtils"; +import { expectThrow } from "./expectThrow"; +import { Constants } from "loopringV3.js"; +import { BalanceSnapshot, ExchangeTestUtil } from "./testExchangeUtil"; +import { AuthMethod, SpotTrade } from "./types"; +import { SignatureType, sign, verifySignature } from "../util/Signature"; + +const AgentRegistry = artifacts.require("AgentRegistry"); + +const TestConverter = artifacts.require("TestConverter"); +const TestSwapper = artifacts.require("TestSwapper"); + +export class Converter { + public ctx: ExchangeTestUtil; + public contract: any; + public address: string; + + public RATE_BASE: BN; + public TOKEN_BASE: BN; + + public tokenIn: string; + public tokenOut: string; + public ticker: string; + + public totalSupply: BN; + + constructor(ctx: ExchangeTestUtil) { + this.ctx = ctx; + this.RATE_BASE = web3.utils.toWei("1", "ether"); + this.TOKEN_BASE = web3.utils.toWei("1", "ether"); + } + + public async setupConverter( + tokenIn: string, + tokenOut: string, + ticker: string, + rate: BN + ) { + this.tokenIn = tokenIn; + this.tokenOut = tokenOut; + this.ticker = ticker; + + const swapper = await TestSwapper.new(rate, false); + + this.contract = await TestConverter.new( + this.ctx.exchange.address, + swapper.address + ); + await this.contract.initialize( + "Loopring Convert - TOKA -> TOKB", + "LC-TOKA-TOKB", + 18, + this.ctx.getTokenAddress(tokenIn), + this.ctx.getTokenAddress(tokenOut) + ); + await this.contract.approveTokens(); + this.address = this.contract.address; + + await this.ctx.transferBalance( + swapper.address, + tokenOut, + new BN(web3.utils.toWei("20", "ether")) + ); + + await this.ctx.registerToken(this.address, ticker); + } + + public async verifySupply(expectedTotalSupply: BN) { + const totalSupply = await this.contract.totalSupply(); + //console.log("totalSupply: " + totalSupply.toString(10)); + assert(totalSupply.eq(expectedTotalSupply), "unexpected total supply"); + } +} + +contract("LoopringConverter", (accounts: string[]) => { + let ctx: ExchangeTestUtil; + + let agentRegistry: any; + let registryOwner: string; + + const amountIn = new BN(web3.utils.toWei("10", "ether")); + const tradeAmountInA = amountIn.div(new BN(4)); + const tradeAmountInB = amountIn.div(new BN(4)).mul(new BN(3)); + + let broker: string; + let ownerA: string; + let ownerB: string; + + const doConversion = async ( + _tokenIn: string, + _tokenOut: string, + ticker: string, + rate: BN, + expectedSuccess: boolean, + doPhase2: boolean = true + ) => { + const RATE_BASE = new BN(web3.utils.toWei("1", "ether")); + + const converter = new Converter(ctx); + await converter.setupConverter(_tokenIn, _tokenOut, ticker, rate); + + let minAmountOut = amountIn.mul(rate).div(RATE_BASE); + if (!expectedSuccess) { + minAmountOut = minAmountOut.add(new BN(1)); + } + + //console.log("broker: " + broker); + //console.log("converter : " + converter.address); + //console.log("amountIn : " + amountIn.toString(10)); + //console.log("minAmountOut : " + minAmountOut.toString(10)); + + // Phase 1 + { + const ringA: SpotTrade = { + orderA: { + owner: broker, + tokenS: converter.ticker, + tokenB: converter.tokenIn, + amountS: tradeAmountInA, + amountB: tradeAmountInA, + feeBips: 0, + balanceS: new BN(0), + balanceB: new BN(1) + }, + orderB: { + owner: ownerA, + tokenS: converter.tokenIn, + tokenB: converter.ticker, + amountS: tradeAmountInA, + amountB: tradeAmountInA, + feeBips: 0 + }, + expected: { + orderA: { filledFraction: 1.0, spread: new BN(0) }, + orderB: { filledFraction: 1.0 } + } + }; + await ctx.setupRing(ringA); + + const ringB: SpotTrade = { + orderA: { + owner: broker, + tokenS: converter.ticker, + tokenB: converter.tokenIn, + amountS: tradeAmountInB, + amountB: tradeAmountInB, + feeBips: 0, + balanceS: new BN(0), + balanceB: new BN(1) + }, + orderB: { + owner: ownerB, + tokenS: converter.tokenIn, + tokenB: converter.ticker, + amountS: tradeAmountInB, + amountB: tradeAmountInB, + feeBips: 0 + }, + expected: { + orderA: { filledFraction: 1.0, spread: new BN(0) }, + orderB: { filledFraction: 1.0 } + } + }; + await ctx.setupRing(ringB); + + await ctx.flashMint(broker, converter.ticker, amountIn); + + await ctx.sendRing(ringA); + await ctx.sendRing(ringB); + + await ctx.requestWithdrawal( + broker, + converter.tokenIn, + amountIn, + converter.tokenIn, + new BN(0), + { to: converter.address } + ); + + await ctx.addCallback( + converter.address, + converter.contract.contract.methods + .deposit(amountIn, minAmountOut, web3.utils.hexToBytes("0x")) + .encodeABI(), + false + ); + + await ctx.submitTransactions(); + await ctx.submitPendingBlocks(); + } + + await converter.verifySupply(amountIn); + + if (!doPhase2) { + return converter; + } + + // Check result of phase 1 on the vault + const failed = await converter.contract.failed(); + //console.log("failed: " + failed); + assert.equal(failed, !expectedSuccess, "Conversion status unexpected!"); + + const tokenOut = failed ? converter.tokenIn : converter.tokenOut; + //const balance = await ctx.getOnchainBalance( + // converter.contract.address, + // tokenOut + //); + //console.log("Token: " + tokenOut); + //console.log("Balance: " + balance.toString(10)); + + const amountOut = failed ? amountIn : amountIn.mul(rate).div(RATE_BASE); + const tradeAmountOutA = amountOut.div(new BN(4)); + const tradeAmountOutB = amountOut.div(new BN(4)).mul(new BN(3)); + + //console.log("tradeAmountInA: " + tradeAmountInA.toString(10)); + //console.log("tradeAmountOutA: " + tradeAmountOutA.toString(10)); + + // Phase 2 + { + const ringA: SpotTrade = { + orderA: { + owner: broker, + tokenS: tokenOut, + tokenB: converter.ticker, + amountS: tradeAmountOutA, + amountB: tradeAmountInA, + feeBips: 0 + }, + orderB: { + owner: ownerA, + tokenS: converter.ticker, + tokenB: tokenOut, + amountS: tradeAmountInA, + amountB: tradeAmountOutA, + feeBips: 0, + balanceS: new BN(0), + balanceB: new BN(0) + }, + expected: { + orderA: { filledFraction: 1.0, spread: new BN(0) }, + orderB: { filledFraction: 1.0 } + } + }; + await ctx.setupRing(ringA, true, true, false, false); + + const ringB: SpotTrade = { + orderA: { + owner: broker, + tokenS: tokenOut, + tokenB: converter.ticker, + amountS: tradeAmountOutB, + amountB: tradeAmountInB, + feeBips: 0 + }, + orderB: { + owner: ownerB, + tokenS: converter.ticker, + tokenB: tokenOut, + amountS: tradeAmountInB, + amountB: tradeAmountOutB, + feeBips: 20, + balanceS: new BN(0), + balanceB: new BN(0) + }, + expected: { + orderA: { filledFraction: 1.0, spread: new BN(0) }, + orderB: { filledFraction: 1.0 } + } + }; + await ctx.setupRing(ringB, true, true, false, false); + + await ctx.flashMint(broker, tokenOut, amountOut); + + await ctx.sendRing(ringA); + await ctx.sendRing(ringB); + + await ctx.requestWithdrawal( + broker, + converter.ticker, + amountIn, + converter.ticker, + new BN(0), + { to: ctx.operator.address } + ); + + //console.log("amountIn: " + amountIn.toString(10)); + //console.log("amountOut: " + amountOut.toString(10)); + + await ctx.addCallback( + converter.address, + converter.contract.contract.methods + .withdraw(broker, amountIn, amountOut) + .encodeABI(), + false + ); + + await ctx.submitTransactions(); + await ctx.submitPendingBlocks(); + + //const balance = await converter.contract.balanceOf(converter.address); + + await converter.verifySupply(new BN(0)); + } + }; + + before(async () => { + ctx = new ExchangeTestUtil(); + await ctx.initialize(accounts); + + broker = ctx.testContext.orderOwners[11]; + ownerA = ctx.testContext.orderOwners[12]; + ownerB = ctx.testContext.orderOwners[13]; + }); + + after(async () => { + await ctx.stop(); + }); + + beforeEach(async () => { + // Fresh Exchange for each test + await ctx.createExchange(ctx.testContext.stateOwners[0], { + setupTestState: true, + deterministic: true + }); + + // Create the agent registry + registryOwner = accounts[7]; + agentRegistry = await AgentRegistry.new({ from: registryOwner }); + + // Register it on the exchange contract + const wrapper = await ctx.contracts.ExchangeV3.at(ctx.operator.address); + await wrapper.setAgentRegistry(agentRegistry.address, { + from: ctx.exchangeOwner + }); + }); + + describe("Converter", function() { + this.timeout(0); + + [true, false].forEach(function(success) { + [ + new BN(web3.utils.toWei("1.0", "ether")), + new BN(web3.utils.toWei("0.5", "ether")), + new BN(web3.utils.toWei("2.0", "ether")) + ].forEach(function(rate) { + it( + (success ? "Successful" : "Failed") + + " conversion ERC20 -> ERC20 - rate: " + + rate.toString(10), + async () => { + await doConversion("GTO", "WETH", "vETH", rate, success); + } + ); + + it( + (success ? "Successful" : "Failed") + + " conversion ETH -> ERC20 - rate: " + + rate.toString(10), + async () => { + await doConversion("ETH", "GTO", "vETH", rate, success); + } + ); + + it( + (success ? "Successful" : "Failed") + + " conversion ERC20 -> ETH - rate: " + + rate.toString(10), + async () => { + await doConversion("GTO", "ETH", "vETH", rate, success); + } + ); + }); + }); + + it("Manual withdrawal", async () => { + const rate = new BN(web3.utils.toWei("1.0", "ether")); + const converter = await doConversion( + "ETH", + "WETH", + "vETH", + rate, + true, + false + ); + + await ctx.requestWithdrawal( + ownerA, + converter.ticker, + tradeAmountInA, + converter.ticker, + new BN(0) + ); + + await ctx.submitTransactions(); + await ctx.submitPendingBlocks(); + + const snapshot = new BalanceSnapshot(ctx); + await snapshot.transfer( + converter.address, + ownerA, + converter.tokenOut, + tradeAmountInA, + "converter", + "from" + ); + + await converter.verifySupply(amountIn); + await converter.contract.withdraw(ownerA, tradeAmountInA, new BN(0), { + from: ownerA + }); + await converter.verifySupply(amountIn.sub(tradeAmountInA)); + + // Verify balances + await snapshot.verifyBalances(); + }); + }); +}); diff --git a/packages/loopring_v3/test/testDebugTools.ts b/packages/loopring_v3/test/testDebugTools.ts index 6b5f37d13..c73a93575 100644 --- a/packages/loopring_v3/test/testDebugTools.ts +++ b/packages/loopring_v3/test/testDebugTools.ts @@ -3,7 +3,7 @@ import fs = require("fs"); import { AmmPool } from "./ammUtils"; import { Constants } from "loopringV3.js"; import { ExchangeTestUtil, OnchainBlock } from "./testExchangeUtil"; -import { BlockCallback, GasTokenConfig } from "./types"; +import { TransactionReceiverCallback } from "./types"; import { calculateCalldataCost, compressZeros } from "loopringV3.js"; contract("Exchange", (accounts: string[]) => { @@ -71,7 +71,7 @@ contract("Exchange", (accounts: string[]) => { const useCompression = false; const onchainBlocks: OnchainBlock[] = []; - const blockCallbacks: BlockCallback[][] = []; + const transactionReceiverCallback: TransactionReceiverCallback[][] = []; for (const blockName of blockNames) { const baseFilename = blockDirectory + blockName; //const auxDataFilename = baseFilename + "_auxiliaryData.json"; @@ -124,31 +124,26 @@ contract("Exchange", (accounts: string[]) => { console.log(onchainBlock); // Read the AMM transactions - const callbacks: BlockCallback[] = []; + const callbacks: TransactionReceiverCallback[] = []; for (const ammTx of blockInfo.ammTransactions) { - callbacks.push(AmmPool.getBlockCallback(ammTx)); + callbacks.push(AmmPool.getTransactionReceiverCallback(ammTx)); } //console.log(callbacks); onchainBlocks.push(onchainBlock); - blockCallbacks.push(callbacks); + transactionReceiverCallback.push(callbacks); } const submitBlocksTxData = ctx.getSubmitCallbackData(onchainBlocks); console.log(submitBlocksTxData); - const gasTokenConfig: GasTokenConfig = { - gasTokenVault: Constants.zeroAddress, - maxToBurn: 0, - expectedGasRefund: 0, - calldataCost: 0 - }; - // LoopringIOExchangeOwner.submitBlocksWithCallbacks const withCallbacksParameters = ctx.getSubmitBlocksWithCallbacksData( useCompression, submitBlocksTxData, - blockCallbacks + transactionReceiverCallback, + [], + [] ); console.log(withCallbacksParameters); diff --git a/packages/loopring_v3/test/testExchangeAgents.ts b/packages/loopring_v3/test/testExchangeAgents.ts index ace959339..f4a4edd2e 100644 --- a/packages/loopring_v3/test/testExchangeAgents.ts +++ b/packages/loopring_v3/test/testExchangeAgents.ts @@ -202,12 +202,12 @@ contract("Exchange", (accounts: string[]) => { "UNAUTHORIZED" ); - await expectThrow( - exchange.onchainTransferFrom(ownerA, ownerB, token, new BN(0), { - from: agent - }), - "UNAUTHORIZED" - ); + // await expectThrow( + // exchange.onchainTransferFrom(ownerA, ownerB, token, new BN(0), { + // from: agent + // }), + // "UNAUTHORIZED" + // ); // Authorize the agent await registerUserAgentChecked(agent, true, ownerA); @@ -234,12 +234,12 @@ contract("Exchange", (accounts: string[]) => { } ); - await exchange.onchainTransferFrom(ownerA, ownerB, token, new BN(0), { - from: agent - }); + // await exchange.onchainTransferFrom(ownerA, ownerB, token, new BN(0), { + // from: agent + // }); }); - it("agent should be able to transfer onchain funds", async () => { + it.skip("agent should be able to transfer onchain funds", async () => { await createExchange(); const amount = new BN(web3.utils.toWei("412.8", "ether")); diff --git a/packages/loopring_v3/test/testExchangeNonReentrant.ts b/packages/loopring_v3/test/testExchangeNonReentrant.ts index e807a7b64..68c9123fa 100644 --- a/packages/loopring_v3/test/testExchangeNonReentrant.ts +++ b/packages/loopring_v3/test/testExchangeNonReentrant.ts @@ -3,6 +3,7 @@ import fs = require("fs"); import { Constants, BlockType } from "loopringV3.js"; import { expectThrow } from "./expectThrow"; import { ExchangeTestUtil, OnchainBlock } from "./testExchangeUtil"; +import { FlashMint } from "./types"; contract("Exchange", (accounts: string[]) => { let exchangeTestUtil: ExchangeTestUtil; @@ -118,6 +119,15 @@ contract("Exchange", (accounts: string[]) => { "ETH" ); values.push(proof); + } else if ( + input.internalType.startsWith("struct ExchangeData.FlashMint[]") + ) { + const flashMint: FlashMint = { + to: Constants.zeroAddress, + token: Constants.zeroAddress, + amount: "0" + }; + values.push([flashMint]); } else if (input.type.startsWith("uint256[][]")) { values.push([new Array(1).fill("0")]); } else if (input.type.startsWith("uint256[]")) { diff --git a/packages/loopring_v3/test/testExchangeUtil.ts b/packages/loopring_v3/test/testExchangeUtil.ts index ebc013474..6a8229980 100644 --- a/packages/loopring_v3/test/testExchangeUtil.ts +++ b/packages/loopring_v3/test/testExchangeUtil.ts @@ -33,8 +33,10 @@ import { AmmUpdate, AuthMethod, Block, - BlockCallback, + TransactionReceiverCallback, Deposit, + FlashMint, + Callback, Transfer, Noop, OrderInfo, @@ -478,7 +480,7 @@ export class ExchangeTestUtil { public explorer: Explorer; - public blockSizes = [8]; + public blockSizes = [8, 16]; public loopringV3: any; public blockVerifier: any; @@ -535,7 +537,9 @@ export class ExchangeTestUtil { public deterministic: boolean = false; private pendingTransactions: TxType[][] = []; - private pendingBlockCallbacks: BlockCallback[][] = []; + private pendingTransactionReceiverCallbacks: TransactionReceiverCallback[][] = []; + private pendingFlashMints: FlashMint[][] = []; + private pendingCallbacks: Callback[][] = []; private storageIDGenerator: number = 0; @@ -573,13 +577,15 @@ export class ExchangeTestUtil { // { from: this.testContext.deployer } // ); - await this.loopringV3.updateProtocolFeeSettings(50, 0, { + await this.loopringV3.updateProtocolFeeSettings(0, 0, { from: this.testContext.deployer }); for (let i = 0; i < this.MAX_NUM_EXCHANGES; i++) { this.pendingTransactions.push([]); - this.pendingBlockCallbacks.push([]); + this.pendingTransactionReceiverCallbacks.push([]); + this.pendingFlashMints.push([]); + this.pendingCallbacks.push([]); this.pendingBlocks.push([]); this.blocks.push([]); @@ -654,6 +660,18 @@ export class ExchangeTestUtil { }); } + public async getEvents(contract: any, event: string) { + const eventArr: any = await this.getEventsFromContract( + contract, + event, + web3.eth.blockNumber + ); + const items = eventArr.map((eventObj: any) => { + return eventObj.args; + }); + return items; + } + // This works differently from truffleAssert.eventEmitted in that it also is able to // get events emmitted in `deep contracts` (i.e. events not emmitted in the contract // the function got called in). @@ -1010,7 +1028,7 @@ export class ExchangeTestUtil { const deposit = await this.deposit( order.owner, order.owner, - order.tokenS, + balanceS.gt(new BN(0)) ? order.tokenS : "ETH", balanceS ); order.accountID = deposit.accountID; @@ -1132,9 +1150,6 @@ export class ExchangeTestUtil { ? options.amountDepositedCanDiffer : this.exchange; - //console.log("token:" + token); - //console.log("amount:" + amount.toString(10)); - if (!token.startsWith("0x")) { token = this.testContext.tokenSymbolAddrMap.get(token); } @@ -1246,6 +1261,31 @@ export class ExchangeTestUtil { return deposit; } + public async flashMint(owner: string, token: string, amount: BN) { + this.requestDeposit(owner, token, amount); + this.addFlashMint(owner, token, amount); + } + + public addFlashMint(owner: string, token: string, amount: BN) { + const flashMint: FlashMint = { + to: owner, + token: this.getTokenAddress(token), + amount: amount.toString(10) + }; + this.pendingFlashMints[this.exchangeId].push(flashMint); + return flashMint; + } + + public addCallback(to: string, data: string, before: boolean) { + const callback: Callback = { + to, + data, + before + }; + this.pendingCallbacks[this.exchangeId].push(callback); + return callback; + } + public hexToDecString(hex: string) { return new BN(hex.slice(2), 16).toString(10); } @@ -1306,12 +1346,16 @@ export class ExchangeTestUtil { if (authMethod === AuthMethod.FORCE && !skipForcedAuthentication) { const withdrawalFee = await this.loopringV3.forcedWithdrawalFee(); if (owner != Constants.zeroAddress) { - const numAvailableSlotsBefore = (await this.exchange.getNumAvailableForcedSlots()).toNumber(); + const numAvailableSlotsBefore = ( + await this.exchange.getNumAvailableForcedSlots() + ).toNumber(); await this.exchange.forceWithdraw(signer, token, accountID, { from: signer, value: withdrawalFee }); - const numAvailableSlotsAfter = (await this.exchange.getNumAvailableForcedSlots()).toNumber(); + const numAvailableSlotsAfter = ( + await this.exchange.getNumAvailableForcedSlots() + ).toNumber(); assert.equal( numAvailableSlotsAfter, numAvailableSlotsBefore - 1, @@ -1709,7 +1753,7 @@ export class ExchangeTestUtil { timestamp: 0, transactionHash: "0", internalBlock: txBlock, - callbacks: this.pendingBlockCallbacks[this.exchangeId] + callbacks: this.pendingTransactionReceiverCallbacks[this.exchangeId] }; this.pendingBlocks[this.exchangeId].push(block); this.blocks[this.exchangeId].push(block); @@ -1808,7 +1852,7 @@ export class ExchangeTestUtil { } } - public getCallbackConfig(blockCallbacks: BlockCallback[][]) { + public getCallbackConfig(calls: TransactionReceiverCallback[][]) { interface TxCallback { txIdx: number; numTxs: number; @@ -1821,18 +1865,18 @@ export class ExchangeTestUtil { txCallbacks: TxCallback[]; } - interface CallbackConfig { - blockCallbacks: OnchainBlockCallback[]; + interface TransactionReceiverCallbacks { + callbacks: OnchainBlockCallback[]; receivers: string[]; } - const callbackConfig: CallbackConfig = { - blockCallbacks: [], + const transactionReceiverCallbacks: TransactionReceiverCallbacks = { + callbacks: [], receivers: [] }; //console.log("Block callbacks: "); - for (const [blockIdx, callbacks] of blockCallbacks.entries()) { + for (const [blockIdx, callbacks] of calls.entries()) { //console.log(blockIdx); //console.log(block.callbacks); if (callbacks.length > 0) { @@ -1840,16 +1884,16 @@ export class ExchangeTestUtil { blockIdx, txCallbacks: [] }; - callbackConfig.blockCallbacks.push(onchainBlockCallback); + transactionReceiverCallbacks.callbacks.push(onchainBlockCallback); for (const blockCallback of callbacks) { // Find receiver index - let receiverIdx = callbackConfig.receivers.findIndex( + let receiverIdx = transactionReceiverCallbacks.receivers.findIndex( target => target === blockCallback.target ); if (receiverIdx === -1) { - receiverIdx = callbackConfig.receivers.length; - callbackConfig.receivers.push(blockCallback.target); + receiverIdx = transactionReceiverCallbacks.receivers.length; + transactionReceiverCallbacks.receivers.push(blockCallback.target); } // Add the block callback to the list onchainBlockCallback.txCallbacks.push({ @@ -1866,7 +1910,7 @@ export class ExchangeTestUtil { //for (const bc of callbackConfig.blockCallbacks) { // console.log(bc); //} - return callbackConfig; + return transactionReceiverCallbacks; } public setPreApprovedTransactions(blocks: Block[]) { @@ -1876,8 +1920,13 @@ export class ExchangeTestUtil { for (const auxiliaryData of block.auxiliaryData) { if (auxiliaryData[0] === Number(blockCallback.txIdx) + i) { auxiliaryData[1] = true; - // No auxiliary data needed for the tx - auxiliaryData[2] = "0x"; + if ( + block.internalBlock.transactions[auxiliaryData[0]].txType !== + "Withdraw" + ) { + // No auxiliary data needed for the tx + auxiliaryData[2] = "0x"; + } } } } @@ -1946,7 +1995,9 @@ export class ExchangeTestUtil { .submitBlocksWithCallbacks( parameters.isDataCompressed, parameters.data, - parameters.callbackConfig + parameters.callbackConfig, + parameters.flashMints, + parameters.callbacks ) .encodeABI(); } @@ -1954,18 +2005,22 @@ export class ExchangeTestUtil { public getSubmitBlocksWithCallbacksData( isDataCompressed: boolean, txData: string, - blockCallbacks: BlockCallback[][] + transactionReceiverCallbacks: TransactionReceiverCallback[][], + flashMints: FlashMint[], + callbacks: Callback[] ) { const data = isDataCompressed ? compressZeros(txData) : txData; //console.log(data); // Block callbacks - const callbackConfig = this.getCallbackConfig(blockCallbacks); + const callbackConfig = this.getCallbackConfig(transactionReceiverCallbacks); return { isDataCompressed, data, - callbackConfig + callbackConfig, + flashMints, + callbacks }; } @@ -2048,7 +2103,7 @@ export class ExchangeTestUtil { // Prepare block data const onchainBlocks: OnchainBlock[] = []; - const blockCallbacks: BlockCallback[][] = []; + const transactionReceiverCallbacks: TransactionReceiverCallback[][] = []; for (const block of blocks) { //console.log(block.blockIdx); const onchainBlock = this.getOnchainBlock( @@ -2062,7 +2117,7 @@ export class ExchangeTestUtil { block.blockVersion ); onchainBlocks.push(onchainBlock); - blockCallbacks.push(block.callbacks); + transactionReceiverCallbacks.push(block.callbacks); } // Callback that allows modifying the blocks @@ -2070,52 +2125,30 @@ export class ExchangeTestUtil { testCallback(onchainBlocks, blocks); } - const numBlocksSubmittedBefore = (await this.exchange.getBlockHeight()).toNumber(); + const numBlocksSubmittedBefore = ( + await this.exchange.getBlockHeight() + ).toNumber(); // Forced requests - const numAvailableSlotsBefore = (await this.exchange.getNumAvailableForcedSlots()).toNumber(); + const numAvailableSlotsBefore = ( + await this.exchange.getNumAvailableForcedSlots() + ).toNumber(); // SubmitBlocks raw tx data const txData = this.getSubmitCallbackData(onchainBlocks); //console.log(txData); - // const gasTokenConfig: GasTokenConfig = { - // gasTokenVault: Constants.zeroAddress, - // maxToBurn: 0, - // expectedGasRefund: 0, - // calldataCost: 0 - // }; - const parameters = this.getSubmitBlocksWithCallbacksData( true, txData, - blockCallbacks + transactionReceiverCallbacks, + this.pendingFlashMints[this.exchangeId], + this.pendingCallbacks[this.exchangeId] ); // Submit the blocks onchain const operatorContract = this.operator ? this.operator : this.exchange; - let bestGasTokensToBurn = 0; - /*let bestGasUsed = 20000000; - for (let i = 0; i < 15; i++) { - gasTokenConfig.maxToBurn = i; - const gasUsed = await operatorContract.submitBlocksWithCallbacks.estimateGas( - parameters.isDataCompressed, - parameters.data, - parameters.callbackConfig, - gasTokenConfig, - { from: this.exchangeOperator, gasPrice: 0 } - ); - if (gasUsed < bestGasUsed) { - bestGasUsed = gasUsed; - bestGasTokensToBurn = i; - } - console.log("" + i + ": " + gasUsed); - } - console.log("Best gas used: " + bestGasUsed); - console.log("Num gas tokens burned: " + bestGasTokensToBurn);*/ - // gasTokenConfig.maxToBurn = bestGasTokensToBurn; - let numDeposits = 0; for (const block of blocks) { for (const tx of block.internalBlock.transactions) { @@ -2134,6 +2167,8 @@ export class ExchangeTestUtil { parameters.isDataCompressed, parameters.data, parameters.callbackConfig, + parameters.flashMints, + parameters.callbacks, //txData, { from: this.exchangeOperator, gasPrice: 0 } ); @@ -2165,8 +2200,13 @@ export class ExchangeTestUtil { ); const ethBlock = await web3.eth.getBlock(tx.receipt.blockNumber); + this.pendingFlashMints[this.exchangeId] = []; + this.pendingCallbacks[this.exchangeId] = []; + // Check number of blocks submitted - const numBlocksSubmittedAfter = (await this.exchange.getBlockHeight()).toNumber(); + const numBlocksSubmittedAfter = ( + await this.exchange.getBlockHeight() + ).toNumber(); assert.equal( numBlocksSubmittedAfter, numBlocksSubmittedBefore + blocks.length, @@ -2230,7 +2270,9 @@ export class ExchangeTestUtil { } // Forced requests - const numAvailableSlotsAfter = (await this.exchange.getNumAvailableForcedSlots()).toNumber(); + const numAvailableSlotsAfter = ( + await this.exchange.getNumAvailableForcedSlots() + ).toNumber(); let numForcedRequestsProcessed = 0; for (const block of blocks) { for (const tx of block.internalBlock.transactions) { @@ -2250,14 +2292,16 @@ export class ExchangeTestUtil { } public addBlockCallback(target: string) { - const blockCallback: BlockCallback = { + const transactionReceiverCallback: TransactionReceiverCallback = { target, auxiliaryData: Constants.emptyBytes, txIdx: this.pendingTransactions[this.exchangeId].length, numTxs: 0 }; - this.pendingBlockCallbacks[this.exchangeId].push(blockCallback); - return blockCallback; + this.pendingTransactionReceiverCallbacks[this.exchangeId].push( + transactionReceiverCallback + ); + return transactionReceiverCallback; } public async submitPendingBlocks(testCallback?: any) { @@ -2376,7 +2420,9 @@ export class ExchangeTestUtil { } const ammTransactions: any[] = []; - for (const callback of this.pendingBlockCallbacks[this.exchangeId]) { + for (const callback of this.pendingTransactionReceiverCallbacks[ + this.exchangeId + ]) { ammTransactions.push(callback.tx); } @@ -2467,7 +2513,7 @@ export class ExchangeTestUtil { } this.pendingTransactions[exchangeID] = []; - this.pendingBlockCallbacks[exchangeID] = []; + this.pendingTransactionReceiverCallbacks[exchangeID] = []; return blocks; } @@ -2647,10 +2693,22 @@ export class ExchangeTestUtil { return bs.getData(); } - public async registerToken(tokenAddress: string) { - const tx = await this.exchange.registerToken(tokenAddress, { + public async registerToken(tokenAddress: string, symbol?: string) { + const onchainExchangeOwner = await this.exchange.owner(); + let contract = this.exchange; + if (this.operator && this.operator.address == onchainExchangeOwner) { + contract = await this.contracts.ExchangeV3.at(this.operator.address); + } + + // Register it on the exchange contract + const tx = await contract.registerToken(tokenAddress, { from: this.exchangeOwner }); + if (symbol) { + this.testContext.tokenSymbolAddrMap.set(symbol, tokenAddress); + } + + await this.addTokenToMaps(tokenAddress); // logInfo("\x1b[46m%s\x1b[0m", "[TokenRegistration] Gas used: " + tx.receipt.gasUsed); } @@ -2922,8 +2980,16 @@ export class ExchangeTestUtil { } public async transferBalance(to: string, token: string, amount: BN) { - const Token = await this.getTokenContract(token); - await Token.transfer(to, amount, { from: this.testContext.deployer }); + if (token === "ETH" || token === Constants.zeroAddress) { + await web3.eth.sendTransaction({ + from: this.testContext.deployer, + to: to, + value: amount + }); + } else { + const Token = await this.getTokenContract(token); + await Token.transfer(to, amount, { from: this.testContext.deployer }); + } } public evmIncreaseTime(seconds: number) { @@ -2965,14 +3031,14 @@ export class ExchangeTestUtil { } public async advanceBlockTimestamp(seconds: number) { - const previousTimestamp = (await web3.eth.getBlock( - await web3.eth.getBlockNumber() - )).timestamp; + const previousTimestamp = ( + await web3.eth.getBlock(await web3.eth.getBlockNumber()) + ).timestamp; await this.evmIncreaseTime(seconds); await this.evmMine(); - const currentTimestamp = (await web3.eth.getBlock( - await web3.eth.getBlockNumber() - )).timestamp; + const currentTimestamp = ( + await web3.eth.getBlock(await web3.eth.getBlockNumber()) + ).timestamp; assert( Math.abs(currentTimestamp - (previousTimestamp + seconds)) < 60, "Timestamp should have been increased by roughly the expected value" @@ -3535,19 +3601,27 @@ export class ExchangeTestUtil { const tokenAddrDecimalsMap = new Map(); const tokenAddrInstanceMap = new Map(); - const [eth, weth, lrc, gto, rdn, rep, inda, indb, test] = await Promise.all( - [ - null, - this.contracts.WETHToken.deployed(), - this.contracts.LRCToken.deployed(), - this.contracts.GTOToken.deployed(), - this.contracts.RDNToken.deployed(), - this.contracts.REPToken.deployed(), - this.contracts.INDAToken.deployed(), - this.contracts.INDBToken.deployed(), - this.contracts.TESTToken.deployed() - ] - ); + const [ + eth, + weth, + lrc, + gto, + rdn, + rep, + inda, + indb, + test + ] = await Promise.all([ + null, + this.contracts.WETHToken.deployed(), + this.contracts.LRCToken.deployed(), + this.contracts.GTOToken.deployed(), + this.contracts.RDNToken.deployed(), + this.contracts.REPToken.deployed(), + this.contracts.INDAToken.deployed(), + this.contracts.INDBToken.deployed(), + this.contracts.TESTToken.deployed() + ]); const allTokens = [eth, weth, lrc, gto, rdn, rep, inda, indb, test]; diff --git a/packages/loopring_v3/test/testPoseidon.ts b/packages/loopring_v3/test/testPoseidon.ts index e3fd389be..48bf080a4 100644 --- a/packages/loopring_v3/test/testPoseidon.ts +++ b/packages/loopring_v3/test/testPoseidon.ts @@ -17,65 +17,76 @@ contract("Poseidon", (accounts: string[]) => { poseidonContract = await contracts.PoseidonContract.new(); }); - it("Poseidon t5/f6/p52", async () => { - const hasher = Poseidon.createHash(5, 6, 52); - // Test some random hashes - const numIterations = 128; - for (let i = 0; i < numIterations; i++) { - const t = [getRand(), getRand(), getRand(), getRand()]; - const hash = await poseidonContract.hash_t5f6p52( - t[0], - t[1], - t[2], - t[3], - new BN(0) - ); - const expectedHash = hasher(t); - assert.equal(hash, expectedHash, "posseidon hash incorrect"); - } + describe("Poseidon", function() { + this.timeout(0); - // Should not be possible to use an input that is larger than the field - for (let i = 0; i < 5; i++) { - const inputs: BN[] = []; - for (let j = 0; j < 5; j++) { - inputs.push(i === j ? Constants.scalarField : new BN(0)); + it("Poseidon t5/f6/p52", async () => { + const hasher = Poseidon.createHash(5, 6, 52); + // Test some random hashes + const numIterations = 128; + for (let i = 0; i < numIterations; i++) { + const t = [getRand(), getRand(), getRand(), getRand()]; + const hash = await poseidonContract.hash_t5f6p52( + t[0], + t[1], + t[2], + t[3], + new BN(0) + ); + const expectedHash = hasher(t); + assert.equal(hash, expectedHash, "posseidon hash incorrect"); } - await expectThrow( - poseidonContract.hash_t5f6p52(...inputs), - "INVALID_INPUT" - ); - } - }); - it("Poseidon t7/f6/p52", async () => { - const hasher = Poseidon.createHash(7, 6, 52); - // Test some random hashes - const numIterations = 128; - for (let i = 0; i < numIterations; i++) { - const t = [getRand(), getRand(), getRand(), getRand(), getRand(), getRand()]; - const hash = await poseidonContract.hash_t7f6p52( - t[0], - t[1], - t[2], - t[3], - t[4], - t[5], - new BN(0) - ); - const expectedHash = hasher(t); - assert.equal(hash, expectedHash, "posseidon hash incorrect"); - } + // Should not be possible to use an input that is larger than the field + for (let i = 0; i < 5; i++) { + const inputs: BN[] = []; + for (let j = 0; j < 5; j++) { + inputs.push(i === j ? Constants.scalarField : new BN(0)); + } + await expectThrow( + poseidonContract.hash_t5f6p52(...inputs), + "INVALID_INPUT" + ); + } + }); + + it("Poseidon t7/f6/p52", async () => { + const hasher = Poseidon.createHash(7, 6, 52); + // Test some random hashes + const numIterations = 128; + for (let i = 0; i < numIterations; i++) { + const t = [ + getRand(), + getRand(), + getRand(), + getRand(), + getRand(), + getRand() + ]; + const hash = await poseidonContract.hash_t7f6p52( + t[0], + t[1], + t[2], + t[3], + t[4], + t[5], + new BN(0) + ); + const expectedHash = hasher(t); + assert.equal(hash, expectedHash, "posseidon hash incorrect"); + } - // Should not be possible to use an input that is larger than the field - for (let i = 0; i < 7; i++) { - const inputs: BN[] = []; - for (let j = 0; j < 7; j++) { - inputs.push(i === j ? Constants.scalarField : new BN(0)); + // Should not be possible to use an input that is larger than the field + for (let i = 0; i < 7; i++) { + const inputs: BN[] = []; + for (let j = 0; j < 7; j++) { + inputs.push(i === j ? Constants.scalarField : new BN(0)); + } + await expectThrow( + poseidonContract.hash_t7f6p52(...inputs), + "INVALID_INPUT" + ); } - await expectThrow( - poseidonContract.hash_t7f6p52(...inputs), - "INVALID_INPUT" - ); - } + }); }); }); diff --git a/packages/loopring_v3/test/types.ts b/packages/loopring_v3/test/types.ts index 3e1739d59..b0159a2e5 100644 --- a/packages/loopring_v3/test/types.ts +++ b/packages/loopring_v3/test/types.ts @@ -222,7 +222,7 @@ export interface TxBlock { signature?: Signature; } -export interface BlockCallback { +export interface TransactionReceiverCallback { target: string; txIdx: number; numTxs: number; @@ -230,11 +230,16 @@ export interface BlockCallback { tx?: any; } -export interface GasTokenConfig { - gasTokenVault: string; - maxToBurn: number; - expectedGasRefund: number; - calldataCost: number; +export interface FlashMint { + to: string; + token: string; + amount: string; +} + +export interface Callback { + to: string; + data: string; + before: boolean; } export interface Block { @@ -259,7 +264,7 @@ export interface Block { internalBlock: TxBlock; blockInfoData?: any; shutdown?: boolean; - callbacks?: BlockCallback[]; + callbacks?: TransactionReceiverCallback[]; } export interface Account { diff --git a/packages/loopring_v3/truffle.js b/packages/loopring_v3/truffle.js index 2c5a259fc..5daff5d80 100644 --- a/packages/loopring_v3/truffle.js +++ b/packages/loopring_v3/truffle.js @@ -50,7 +50,7 @@ module.exports = { runs: 1000000 } }, - version: "0.7.0" + version: "0.7.6" } }, plugins: ["truffle-plugin-verify", "solidity-coverage"],