diff --git a/contracts/ERC20Safe.sol b/contracts/ERC20Safe.sol index 4f03359..c840759 100644 --- a/contracts/ERC20Safe.sol +++ b/contracts/ERC20Safe.sol @@ -38,10 +38,16 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { uint16 private constant maxBatchSize = 100; uint8 public batchBlockLimit; uint8 public batchSettleLimit; + uint256 public numBuckets; + uint256 public blocksInBucket; + uint256 public defaultSingleTransactionThreshold; + uint256 public defaultAggregateValueThreshold; // Reserved storage slots for future upgrades uint256[10] private __gap; + mapping(uint256 => uint256) public bucketLastUpdatedNonce; + mapping(uint256 => mapping(address => uint256)) public aggregatedValue; mapping(uint256 => Batch) public batches; mapping(address => bool) public whitelistedTokens; mapping(address => bool) public mintBurnTokens; @@ -52,9 +58,29 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { mapping(address => uint256) public mintBalances; mapping(address => uint256) public burnBalances; mapping(uint256 => Deposit[]) public batchDeposits; + mapping(address => uint256) public singleTransactionThreshold; + mapping(address => uint256) public aggregateValueThreshold; event ERC20Deposit(uint112 batchId, uint112 depositNonce); event ERC20SCDeposit(uint112 indexed batchId, uint112 depositNonce, bytes callData); + event TransactionDelayed( + address indexed sender, + address indexed tokenAddress, + uint256 amount, + bytes32 recipientAddress, + bool isLarge + ); + + //optional + event DelayedTransactionProcessed( + address indexed sender, + address indexed tokenAddress, + uint256 amount, + bytes32 recipientAddress, + bool isLarge + ); + + DelayedTransaction[] public delayedTransactions; function initialize() public initializer { __BridgeRole_init(); @@ -66,6 +92,10 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { batchSize = 10; batchBlockLimit = 40; batchSettleLimit = 40; + numBuckets = 24; + blocksInBucket = 300; // 300 blocks = 3600 seconds/12 seconds per block + defaultSingleTransactionThreshold = 1000; //to be set correctly + defaultAggregateValueThreshold = 10000; //to be set correctly } /** @@ -83,7 +113,9 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { bool native, uint256 totalBalance, uint256 mintBalance, - uint256 burnBalance + uint256 burnBalance, + uint256 singleTxThreshold, + uint256 aggregateThreshold ) external onlyAdmin { if (!mintBurn) { require(native, "Only native tokens can be stored!"); @@ -101,6 +133,18 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { require(burnBalance == 0, "Stored tokens must have 0 burn balance!"); initSupply(token, totalBalance); } + + if (singleTxThreshold == 0) { + singleTransactionThreshold[token] = defaultSingleTransactionThreshold; + } else { + singleTransactionThreshold[token] = singleTxThreshold; + } + + if (aggregateThreshold == 0) { + aggregateValueThreshold[token] = defaultAggregateValueThreshold; + } else { + aggregateValueThreshold[token] = aggregateThreshold; + } } /** @@ -186,8 +230,7 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { function deposit(address tokenAddress, uint256 amount, bytes32 recipientAddress) public whenNotPaused { uint112 batchNonce; uint112 depositNonce; - (batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress); - emit ERC20Deposit(depositNonce, batchNonce); + (batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress, ""); } /* @@ -205,19 +248,132 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { * 0x + endpoint_name_length (4 bytes) + endpoint_name + gas_limit (8 bytes) + * 00 (ArgumentsPresentProtocolMarker) */ - function depositWithSCExecution(address tokenAddress, uint256 amount, bytes32 recipientAddress, bytes calldata callData) public whenNotPaused { + function depositWithSCExecution( + address tokenAddress, + uint256 amount, + bytes32 recipientAddress, + bytes memory callData + ) public whenNotPaused { uint112 batchNonce; uint112 depositNonce; - (batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress); - emit ERC20SCDeposit(batchNonce, depositNonce, callData); + (batchNonce, depositNonce) = _deposit_common(tokenAddress, amount, recipientAddress, callData); } - - function _deposit_common(address tokenAddress, uint256 amount, bytes32 recipientAddress) internal returns (uint112 batchNonce, uint112 depositNonce) { + function _deposit_common( + address tokenAddress, + uint256 amount, + bytes32 recipientAddress, + bytes memory callData + ) internal returns (uint112 batchNonce, uint112 depositNonce) { require(whitelistedTokens[tokenAddress], "Unsupported token"); require(amount >= tokenMinLimits[tokenAddress], "Tried to deposit an amount below the minimum specified limit"); require(amount <= tokenMaxLimits[tokenAddress], "Tried to deposit an amount above the maximum specified limit"); + _processDelayedTransactions(tokenAddress); + + uint256 currentBlock = block.number; + uint256 bucketId = (currentBlock / blocksInBucket) % numBuckets; + + _resetBucketIfNeeded(bucketId, tokenAddress, currentBlock); + + if (amount >= singleTransactionThreshold[tokenAddress]) { + _addDelayedTransaction(tokenAddress, amount, recipientAddress, true, callData); + emit TransactionDelayed(msg.sender, tokenAddress, amount, recipientAddress, true); + return (0, 0); + } else { + uint256 totalAggregatedValue = _getTotalAggregatedValue(tokenAddress, currentBlock); + if (totalAggregatedValue + amount <= aggregateValueThreshold[tokenAddress]) { + aggregatedValue[bucketId][tokenAddress] += amount; + (batchNonce, depositNonce) = _processDeposit(tokenAddress, amount, recipientAddress, callData); + return (batchNonce, depositNonce); + } else { + _addDelayedTransaction(tokenAddress, amount, recipientAddress, false, callData); + emit TransactionDelayed(msg.sender, tokenAddress, amount, recipientAddress, false); + return (0, 0); + } + } + } + + function _addDelayedTransaction( + address tokenAddress, + uint256 amount, + bytes32 recipientAddress, + bool isLarge, + bytes memory callData + ) internal { + DelayedTransaction memory dt = DelayedTransaction({ + amount: amount, + tokenAddress: tokenAddress, + sender: msg.sender, + recipientAddress: recipientAddress, + blockAdded: block.number, + isLarge: isLarge, + callData: callData + }); + delayedTransactions.push(dt); + } + + function _processDelayedTransactions(address tokenAddress) internal { + uint256 i = 0; + while (i < delayedTransactions.length) { + DelayedTransaction storage dt = delayedTransactions[i]; + + if (dt.tokenAddress != tokenAddress) { + i++; + continue; + } + + if (_canProcessDelayedTransaction(dt)) { + _processDeposit(dt.tokenAddress, dt.amount, dt.recipientAddress, dt.callData); + + delayedTransactions[i] = delayedTransactions[delayedTransactions.length - 1]; + delayedTransactions.pop(); + + emit DelayedTransactionProcessed( + dt.sender, + dt.tokenAddress, + dt.amount, + dt.recipientAddress, + dt.isLarge + ); + } else { + i++; + } + } + } + + function _canProcessDelayedTransaction(DelayedTransaction storage dt) internal returns (bool) { + uint256 currentBlock = block.number; + uint256 averageBlockTime = 12; + + if (dt.isLarge) { + uint256 blocksIn24Hours = (24 * 60 * 60) / averageBlockTime; + return currentBlock >= dt.blockAdded + blocksIn24Hours; + } else { + uint256 bucketId = (currentBlock / blocksInBucket) % numBuckets; + + _resetBucketIfNeeded(bucketId, dt.tokenAddress, currentBlock); + + uint256 totalAggregatedValue = _getTotalAggregatedValue(dt.tokenAddress, currentBlock); + uint256 threshold = aggregateValueThreshold[dt.tokenAddress]; + + if (totalAggregatedValue + dt.amount <= threshold) { + aggregatedValue[bucketId][dt.tokenAddress] += dt.amount; + return true; + } else if (currentBlock >= dt.blockAdded + ((24 * 60 * 60) / averageBlockTime)) { + return true; + } else { + return false; + } + } + } + + function _processDeposit( + address tokenAddress, + uint256 amount, + bytes32 recipientAddress, + bytes memory callData + ) internal returns (uint112 batchNonce, uint112 depositNonce) { uint64 currentBlockNumber = uint64(block.number); Batch storage batch; @@ -230,7 +386,7 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { batch = batches[batchesCount - 1]; } - uint112 depositNonce = depositsCount + 1; + depositNonce = depositsCount + 1; batchDeposits[batchesCount - 1].push( Deposit(depositNonce, tokenAddress, amount, msg.sender, recipientAddress, DepositStatus.Pending) ); @@ -253,7 +409,68 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { totalBalances[tokenAddress] += amount; erc20.safeTransferFrom(msg.sender, address(this), amount); } - return (batch.nonce, depositNonce); + + batchNonce = batch.nonce; + if (callData.length > 0) { + emit ERC20SCDeposit(batchNonce, depositNonce, callData); + } else { + emit ERC20Deposit(batchNonce, depositNonce); + } + return (batchNonce, depositNonce); + } + + function _getTotalAggregatedValue( + address tokenAddress, + uint256 currentBlock + ) internal returns (uint256 totalAggregatedValue) { + totalAggregatedValue = 0; + + for (uint256 i = 0; i < numBuckets; i++) { + _resetBucketIfNeeded(i, tokenAddress, currentBlock); + totalAggregatedValue += aggregatedValue[i][tokenAddress]; + } + } + + /** + @notice Updates the threshold for a particular token + @param token Address of the ERC20 token + @param amount New threshold for the aggregated value + */ + function setThreshold(address token, uint256 amount) external onlyAdmin { + aggregateValueThreshold[token] = amount; + } + + function getThreshold(address tokenAddress) public view returns (uint256) { + return aggregateValueThreshold[tokenAddress]; + } + + /** + @notice Updates the single transaction threshold for a particular token + @param token Address of the ERC20 token + @param amount New threshold for the aggregated value + */ + function setSingleTxThreshold(address token, uint256 amount) external onlyAdmin { + singleTransactionThreshold[token] = amount; + } + + function getSingleTxThreshold(address tokenAddress) public view returns (uint256) { + return singleTransactionThreshold[tokenAddress]; + } + + /** + @notice Process a delayed transaction immediately + @param index Index of the delayed transaction + */ + function processDelayedTransactionImmediately(uint256 index) external onlyAdmin { + require(index < delayedTransactions.length, "Invalid index"); + DelayedTransaction storage dt = delayedTransactions[index]; + + _processDeposit(dt.tokenAddress, dt.amount, dt.recipientAddress, dt.callData); + + delayedTransactions[index] = delayedTransactions[delayedTransactions.length - 1]; + delayedTransactions.pop(); + + emit DelayedTransactionProcessed(dt.sender, dt.tokenAddress, dt.amount, dt.recipientAddress, dt.isLarge); } /** @@ -420,4 +637,11 @@ contract ERC20Safe is Initializable, BridgeRole, Pausable { function _isTokenMintBurn(address token) internal view returns (bool) { return mintBurnTokens[token]; } + + function _resetBucketIfNeeded(uint256 bucketId, address tokenAddress, uint256 currentBlock) internal { + if (currentBlock - bucketLastUpdatedNonce[bucketId] > blocksInBucket) { + aggregatedValue[bucketId][tokenAddress] = 0; + bucketLastUpdatedNonce[bucketId] = currentBlock; + } + } } diff --git a/contracts/SharedStructs.sol b/contracts/SharedStructs.sol index 289b678..1979aac 100644 --- a/contracts/SharedStructs.sol +++ b/contracts/SharedStructs.sol @@ -43,3 +43,13 @@ struct MvxTransaction { uint256 depositNonce; bytes callData; } + +struct DelayedTransaction { + uint256 amount; + address tokenAddress; + address sender; + bytes32 recipientAddress; + uint256 blockAdded; + bool isLarge; + bytes callData; +} diff --git a/test/Bridge.test.js b/test/Bridge.test.js index 4d6571e..4a24b98 100644 --- a/test/Bridge.test.js +++ b/test/Bridge.test.js @@ -28,7 +28,7 @@ describe("Bridge", async function () { genericErc20 = await deployContract(adminWallet, "GenericERC20", ["TSC", "TSC", 6]); await genericErc20.mint(adminWallet.address, 1000); await genericErc20.approve(erc20Safe.address, 1000); - await erc20Safe.whitelistToken(genericErc20.address, 0, 100, false, true, 0, 0, 0); + await erc20Safe.whitelistToken(genericErc20.address, 0, 100, false, true, 0, 0, 0, 0, 0); await erc20Safe.unpause(); } diff --git a/test/BridgeExecutor.test.js b/test/BridgeExecutor.test.js index cd2e77f..de24e6d 100644 --- a/test/BridgeExecutor.test.js +++ b/test/BridgeExecutor.test.js @@ -29,9 +29,9 @@ describe("BridgeExecutor", function () { async function setupErc20Token() { genericErc20 = await deployContract(adminWallet, "GenericERC20", ["TSC", "TSC", 6]); - await genericErc20.mint(adminWallet.address, 1000); - await genericErc20.approve(erc20Safe.address, 1000); - await erc20Safe.whitelistToken(genericErc20.address, 0, 1000, false, true, 0, 0, 0); + await genericErc20.mint(adminWallet.address, 2000); + await genericErc20.approve(erc20Safe.address, 2000); + await erc20Safe.whitelistToken(genericErc20.address, 0, 10000, false, true, 1000, 0, 0, 0, 0); await erc20Safe.unpause(); } diff --git a/test/MintBurnERC20.test.js b/test/MintBurnERC20.test.js index 3ee9bf5..70f94d4 100644 --- a/test/MintBurnERC20.test.js +++ b/test/MintBurnERC20.test.js @@ -28,7 +28,7 @@ describe("ERC20Safe, MintBurnERC20, and Bridge Interaction", function () { async function setupErc20Token() { mintBurnErc20 = await deployUpgradableContract(adminWallet, "MintBurnERC20", ["Test Token", "TST", 6]); - await erc20Safe.whitelistToken(mintBurnErc20.address, 0, 100, true, false, 0, 0, 0); + await erc20Safe.whitelistToken(mintBurnErc20.address, 0, 100, true, false, 0, 0, 0, 0, 0); await erc20Safe.unpause(); } diff --git a/test/Safe.test.js b/test/Safe.test.js index 39b45ed..a40290e 100644 --- a/test/Safe.test.js +++ b/test/Safe.test.js @@ -7,6 +7,7 @@ const { deployContract, deployUpgradableContract, upgradeContract } = require(". describe("ERC20Safe", function () { const defaultMinAmount = 25; const defaultMaxAmount = 100; + const recipientAddress = Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"); let adminWallet, otherWallet, simpleBoardMember; let boardMembers; @@ -40,7 +41,7 @@ describe("ERC20Safe", function () { describe("ERC20Safe - setting whitelisted tokens works as expected", async function () { it("correctly whitelists token and updates limits", async function () { - await safe.whitelistToken(genericERC20.address, "25", "100", false, true, 0, 0, 0); + await safe.whitelistToken(genericERC20.address, "25", "100", false, true, 0, 0, 0, 0, 0); expect(await safe.isTokenWhitelisted(genericERC20.address)).to.be.true; expect(await safe.getTokenMinLimit(genericERC20.address)).to.eq("25"); expect(await safe.getTokenMaxLimit(genericERC20.address)).to.eq("100"); @@ -56,7 +57,7 @@ describe("ERC20Safe", function () { }); it("reverts", async function () { await expect( - safe.connect(otherWallet).whitelistToken(genericERC20.address, "0", "100", false, true, 0, 0, 0), + safe.connect(otherWallet).whitelistToken(genericERC20.address, "0", "100", false, true, 0, 0, 0, 0, 0), ).to.be.revertedWith("Access Control: sender is not Admin"); await expect(safe.connect(otherWallet).removeTokenFromWhitelist(genericERC20.address)).to.be.revertedWith( "Access Control: sender is not Admin", @@ -104,15 +105,11 @@ describe("ERC20Safe", function () { ); // Creating a batch - await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0); + await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0, 0, 0); await genericERC20.approve(safe.address, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); await genericERC20.mint(adminWallet.address, "1000000"); await safe.unpause(); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); // Thanks to the previous deposit, we have a pending non-final batch await safe.pause(); @@ -135,11 +132,7 @@ describe("ERC20Safe", function () { // will return false const batchSize = await safe.batchSize(); for (let i = 0; i < batchSize; i++) { - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); } await safe.pause(); await expect(safe.connect(adminWallet).setBatchSettleLimit(30)).to.be.revertedWith( @@ -179,13 +172,7 @@ describe("ERC20Safe", function () { describe("ERC20Safe - deposit works as expected", async function () { it("reverts for token that is not whitelisted", async function () { - await expect( - safe.deposit( - genericERC20.address, - 100, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ), - ).to.be.revertedWith("Unsupported token"); + await expect(safe.deposit(genericERC20.address, 100, recipientAddress)).to.be.revertedWith("Unsupported token"); }); describe("contract is paused", async function () { @@ -196,76 +183,49 @@ describe("ERC20Safe", function () { await safe.unpause(); }); it("fails", async function () { - await expect( - safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ), - ).to.be.revertedWith("Pausable: paused"); + await expect(safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress)).to.be.revertedWith( + "Pausable: paused", + ); }); }); describe("when token is whitelisted", async function () { beforeEach(async function () { - await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0); + await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0, 0, 0); await genericERC20.approve(safe.address, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); await genericERC20.mint(adminWallet.address, "1000000"); }); it("reverts when amount is smaller than minimum specified limit", async function () { - await expect( - safe.deposit( - genericERC20.address, - defaultMinAmount - 1, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ), - ).to.be.revertedWith("Tried to deposit an amount below the minimum specified limit"); + await expect(safe.deposit(genericERC20.address, defaultMinAmount - 1, recipientAddress)).to.be.revertedWith( + "Tried to deposit an amount below the minimum specified limit", + ); }); it("reverts when amount is bigger than maximum specified limit", async function () { - await expect( - safe.deposit( - genericERC20.address, - defaultMaxAmount + 1, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ), - ).to.be.revertedWith("Tried to deposit an amount above the maximum specified limit"); + await expect(safe.deposit(genericERC20.address, defaultMaxAmount + 1, recipientAddress)).to.be.revertedWith( + "Tried to deposit an amount above the maximum specified limit", + ); }); it("should emit event in case of deposit success", async function () { const callData = encodeCallData("depositEndpoint", 500000, [25, "someArgument"]); await expect( - safe - .connect(adminWallet) - .depositWithSCExecution( - genericERC20.address, - 25, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - callData, - ), + safe.connect(adminWallet).depositWithSCExecution(genericERC20.address, 25, recipientAddress, callData), ) .to.emit(safe, "ERC20SCDeposit") .withArgs(1, 1, callData); }); it("increments depositsCount", async () => { - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); expect(await safe.depositsCount()).to.equal(1); }); it("updates the lastUpdatedBlockNumber on the batch", async function () { await safe.setBatchBlockLimit(1); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); const batchNonce = await safe.batchesCount(); const [batchAfterFirstTx, isFirstFinal] = await safe.getBatch(batchNonce); @@ -275,11 +235,7 @@ describe("ERC20Safe", function () { await network.provider.send("evm_mine"); } - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); await network.provider.send("evm_mine"); const [batchAfterSecondTx, isSecondFinal] = await safe.getBatch(batchNonce); @@ -292,24 +248,12 @@ describe("ERC20Safe", function () { await safe.setBatchSize(2); expect(await safe.batchesCount()).to.be.eq(0); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); expect(await safe.batchesCount()).to.be.eq(1); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); expect(await safe.batchesCount()).to.be.eq(1); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); expect(await safe.batchesCount()).to.be.eq(2); }); @@ -325,11 +269,7 @@ describe("ERC20Safe", function () { // With 0 extra deposits expect batch count to remain the same expect(await safe.batchesCount()).to.be.eq(0); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); expect(await safe.batchesCount()).to.be.eq(1); await network.provider.send("evm_increaseTime", [batchBlockLimit + 1]); @@ -337,11 +277,7 @@ describe("ERC20Safe", function () { await network.provider.send("evm_mine"); } - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); expect(await safe.batchesCount()).to.be.eq(2); }); @@ -356,15 +292,11 @@ describe("ERC20Safe", function () { await network.provider.send("evm_mine"); } } - let depositResp = await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + let depositResp = await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); let receipt = await depositResp.wait(); - expect(receipt.gasUsed).to.be.lt(400000); + expect(receipt.gasUsed).to.be.lt(1000000); } }); }); @@ -392,19 +324,11 @@ describe("ERC20Safe", function () { }); it("sends just the balance above what is actually deposited for whitelited tokens", async function () { - await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0); + await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0, 0, 0); await genericERC20.approve(safe.address, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); await genericERC20.transfer(safe.address, "1"); expect(await genericERC20.balanceOf(adminWallet.address)).to.be.eq("999999999949"); @@ -416,7 +340,7 @@ describe("ERC20Safe", function () { }); it("sends just the balance above what is actually deposited for whitelited tokens - considers bridge transfers", async function () { - await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0); + await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0, 0, 0); await genericERC20.approve(safe.address, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); const mockBridge = await deployUpgradableContract(adminWallet, "BridgeMock", [ @@ -427,16 +351,8 @@ describe("ERC20Safe", function () { ]); await safe.setBridge(mockBridge.address); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); await mockBridge.proxyTransfer(genericERC20.address, defaultMinAmount, adminWallet.address); @@ -452,7 +368,7 @@ describe("ERC20Safe", function () { describe("ERC20Safe - getBatch and getDeposits work as expected", async function () { beforeEach(async function () { - await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0); + await safe.whitelistToken(genericERC20.address, defaultMinAmount, defaultMaxAmount, false, true, 0, 0, 0, 0, 0); await genericERC20.approve(safe.address, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); await genericERC20.mint(adminWallet.address, "1000000"); }); @@ -461,11 +377,7 @@ describe("ERC20Safe", function () { await safe.setBatchSize(3); const batchBlockLimit = parseInt((await safe.batchBlockLimit()).toString()); - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); let [batch, isFinal] = await safe.getBatch(1); let [deposits, areDepositsFinal] = await safe.getDeposits(1); // Just after deposit @@ -479,11 +391,7 @@ describe("ERC20Safe", function () { await network.provider.send("evm_mine"); } - await safe.deposit( - genericERC20.address, - defaultMinAmount, - Buffer.from("c0f0058cea88a2bc1240b60361efb965957038d05f916c42b3f23a2c38ced81e", "hex"), - ); + await safe.deposit(genericERC20.address, defaultMinAmount, recipientAddress); await network.provider.send("evm_increaseTime", [batchBlockLimit - 1]); for (let i = 0; i < batchBlockLimit - 1; i++) { @@ -528,4 +436,105 @@ describe("ERC20Safe", function () { await safe.setBatchSize(currentBatchSize); }); }); + describe("ERC20Safe - transaction processing and delays", function () { + beforeEach(async function () { + // Whitelist token with specific thresholds + await safe.whitelistToken( + genericERC20.address, + defaultMinAmount, + defaultMaxAmount * 10, + false, + true, + 0, + 0, + 0, + 600, + 1000, + ); + + await genericERC20.approve(safe.address, 100000); + await genericERC20.mint(adminWallet.address, 10000); + }); + + it("processes small transactions immediately", async function () { + await expect(safe.deposit(genericERC20.address, 100, recipientAddress)) + .to.emit(safe, "ERC20Deposit") + .withArgs(1, 1); + expect(await safe.depositsCount()).to.equal(1); + }); + + it("delays small transactions exceeding aggregate threshold", async function () { + // Deposit to reach aggregate threshold + await safe.deposit(genericERC20.address, 600, recipientAddress); + await safe.deposit(genericERC20.address, 500, recipientAddress); + + // This deposit should be delayed + await expect(safe.deposit(genericERC20.address, 700, recipientAddress)).to.emit(safe, "TransactionDelayed"); + }); + + it("delays large transactions exceeding singleTxThreshold", async function () { + // This deposit should be delayed + await expect(safe.deposit(genericERC20.address, 800, recipientAddress)) + .to.emit(safe, "TransactionDelayed") + .withArgs(adminWallet.address, genericERC20.address, 800, recipientAddress, true); + }); + + it("processes delayed small transactions after 24 hours", async function () { + // Deposit causing delay + await safe.deposit(genericERC20.address, 600, recipientAddress); + await safe.deposit(genericERC20.address, 400, recipientAddress); + await safe.deposit(genericERC20.address, 200, recipientAddress); // Delayed + + // Simulate 24 hours (14,400 blocks at 6s per block) + for (let i = 0; i < 14400; i++) { + await network.provider.send("evm_mine"); + } + + // Trigger processing of delayed transactions + await expect(safe.deposit(genericERC20.address, 50, recipientAddress)).to.emit( + safe, + "DelayedTransactionProcessed", + ); + }); + + it("processes delayed large transactions after 24 hours", async function () { + // Deposit causing delay + await safe.deposit(genericERC20.address, 700, recipientAddress); // Delayed + + // Simulate 24 hours + for (let i = 0; i < 14400; i++) { + await network.provider.send("evm_mine"); + } + + // Trigger processing of delayed transactions + await expect(safe.deposit(genericERC20.address, 500, recipientAddress)).to.emit( + safe, + "DelayedTransactionProcessed", + ); + }); + + it("admin can process delayed transactions immediately", async function () { + // Deposit causing delay + await safe.deposit(genericERC20.address, 700, recipientAddress); // Delayed + + // Admin processes delayed transaction + await expect(safe.processDelayedTransactionImmediately(0)).to.emit(safe, "DelayedTransactionProcessed"); + }); + + it("does not process delayed transactions before 24 hours", async function () { + // Deposit causing delay + await safe.deposit(genericERC20.address, 700, recipientAddress); // Delayed + + // Simulate less than 24 hours + for (let i = 0; i < 100; i++) { + await network.provider.send("evm_mine"); + } + + // Attempt to trigger processing + await expect(safe.deposit(genericERC20.address, 400, recipientAddress)).to.not.emit( + safe, + "DelayedTransactionProcessed", + ); + }); + }); });