Skip to content

Commit 0ace4f0

Browse files
author
Rahul Maganti
committed
rate_limit: add fuzz test for inbound rate limiting
1 parent d8a2a30 commit 0ace4f0

File tree

1 file changed

+174
-64
lines changed

1 file changed

+174
-64
lines changed

evm/test/RateLimit.t.sol

+174-64
Original file line numberDiff line numberDiff line change
@@ -439,70 +439,6 @@ contract TestRateLimit is Test, IRateLimiterEvents {
439439
nttManager.completeOutboundQueuedTransfer(0);
440440
}
441441

442-
function testFuzz_outboundRateLimitShouldQueue(uint256 limitAmt, uint256 transferAmt) public {
443-
// setup
444-
address user_A = address(0x123);
445-
address user_B = address(0x456);
446-
DummyToken token = DummyToken(nttManager.token());
447-
uint8 decimals = token.decimals();
448-
449-
// inputs
450-
uint256 totalAmt = (type(uint64).max) / (10 ** decimals);
451-
// avoids the ZeroAmount() error
452-
// cannot transfer more than what's available
453-
vm.assume(transferAmt > 0 && transferAmt <= totalAmt);
454-
// this ensures that the transfer is always queued up
455-
vm.assume(limitAmt < transferAmt);
456-
457-
// mint
458-
token.mintDummy(address(user_A), totalAmt * (10 ** decimals));
459-
uint256 outboundLimit = limitAmt * (10 ** decimals);
460-
nttManager.setOutboundLimit(outboundLimit);
461-
462-
vm.startPrank(user_A);
463-
464-
// initiate transfer
465-
uint256 transferAmount = transferAmt * (10 ** decimals);
466-
token.approve(address(nttManager), transferAmount);
467-
468-
// shouldQueue == true
469-
uint64 qSeq = nttManager.transfer(
470-
transferAmount, chainId, toWormholeFormat(user_B), true, new bytes(1)
471-
);
472-
473-
// assert that the transfer got queued up
474-
assertEq(qSeq, 0);
475-
IRateLimiter.OutboundQueuedTransfer memory qt = nttManager.getOutboundQueuedTransfer(0);
476-
assertEq(qt.amount.getAmount(), transferAmount.trim(decimals, decimals).getAmount());
477-
assertEq(qt.recipientChain, chainId);
478-
assertEq(qt.recipient, toWormholeFormat(user_B));
479-
assertEq(qt.txTimestamp, initialBlockTimestamp);
480-
481-
// assert that the contract also locked funds from the user
482-
assertEq(token.balanceOf(address(user_A)), totalAmt * (10 ** decimals) - transferAmount);
483-
assertEq(token.balanceOf(address(nttManager)), transferAmount);
484-
485-
// elapse rate limit duration - 1
486-
uint256 durationElapsedTime = initialBlockTimestamp + nttManager.rateLimitDuration();
487-
vm.warp(durationElapsedTime - 1);
488-
489-
// assert that transfer still can't be completed
490-
bytes4 stillQueuedSelector =
491-
bytes4(keccak256("OutboundQueuedTransferStillQueued(uint64,uint256)"));
492-
vm.expectRevert(abi.encodeWithSelector(stillQueuedSelector, 0, initialBlockTimestamp));
493-
nttManager.completeOutboundQueuedTransfer(0);
494-
495-
// now complete transfer
496-
vm.warp(durationElapsedTime);
497-
uint64 seq = nttManager.completeOutboundQueuedTransfer(0);
498-
assertEq(seq, 0);
499-
500-
// now ensure transfer was removed from queue
501-
bytes4 notFoundSelector = bytes4(keccak256("OutboundQueuedTransferNotFound(uint64)"));
502-
vm.expectRevert(abi.encodeWithSelector(notFoundSelector, 0));
503-
nttManager.completeOutboundQueuedTransfer(0);
504-
}
505-
506442
function test_inboundRateLimit_simple() public {
507443
address user_B = address(0x456);
508444

@@ -762,4 +698,178 @@ contract TestRateLimit is Test, IRateLimiterEvents {
762698
assertEq(inboundLimitParams.lastTxTimestamp, sendAgainTime);
763699
}
764700
}
701+
702+
function testFuzz_outboundRateLimitShouldQueue(uint256 limitAmt, uint256 transferAmt) public {
703+
// setup
704+
address user_A = address(0x123);
705+
address user_B = address(0x456);
706+
DummyToken token = DummyToken(nttManager.token());
707+
uint8 decimals = token.decimals();
708+
709+
// inputs
710+
uint256 totalAmt = (type(uint64).max) / (10 ** decimals);
711+
// avoids the ZeroAmount() error
712+
// cannot transfer more than what's available
713+
vm.assume(transferAmt > 0 && transferAmt <= totalAmt);
714+
// this ensures that the transfer is always queued up
715+
vm.assume(limitAmt < transferAmt);
716+
717+
// mint
718+
token.mintDummy(address(user_A), totalAmt * (10 ** decimals));
719+
uint256 outboundLimit = limitAmt * (10 ** decimals);
720+
nttManager.setOutboundLimit(outboundLimit);
721+
722+
vm.startPrank(user_A);
723+
724+
// initiate transfer
725+
uint256 transferAmount = transferAmt * (10 ** decimals);
726+
token.approve(address(nttManager), transferAmount);
727+
728+
// shouldQueue == true
729+
uint64 qSeq = nttManager.transfer(
730+
transferAmount, chainId, toWormholeFormat(user_B), true, new bytes(1)
731+
);
732+
733+
// assert that the transfer got queued up
734+
assertEq(qSeq, 0);
735+
IRateLimiter.OutboundQueuedTransfer memory qt = nttManager.getOutboundQueuedTransfer(0);
736+
assertEq(qt.amount.getAmount(), transferAmount.trim(decimals, decimals).getAmount());
737+
assertEq(qt.recipientChain, chainId);
738+
assertEq(qt.recipient, toWormholeFormat(user_B));
739+
assertEq(qt.txTimestamp, initialBlockTimestamp);
740+
741+
// assert that the contract also locked funds from the user
742+
assertEq(token.balanceOf(address(user_A)), totalAmt * (10 ** decimals) - transferAmount);
743+
assertEq(token.balanceOf(address(nttManager)), transferAmount);
744+
745+
// elapse rate limit duration - 1
746+
uint256 durationElapsedTime = initialBlockTimestamp + nttManager.rateLimitDuration();
747+
vm.warp(durationElapsedTime - 1);
748+
749+
// assert that transfer still can't be completed
750+
bytes4 stillQueuedSelector =
751+
bytes4(keccak256("OutboundQueuedTransferStillQueued(uint64,uint256)"));
752+
vm.expectRevert(abi.encodeWithSelector(stillQueuedSelector, 0, initialBlockTimestamp));
753+
nttManager.completeOutboundQueuedTransfer(0);
754+
755+
// now complete transfer
756+
vm.warp(durationElapsedTime);
757+
uint64 seq = nttManager.completeOutboundQueuedTransfer(0);
758+
assertEq(seq, 0);
759+
760+
// now ensure transfer was removed from queue
761+
bytes4 notFoundSelector = bytes4(keccak256("OutboundQueuedTransferNotFound(uint64)"));
762+
vm.expectRevert(abi.encodeWithSelector(notFoundSelector, 0));
763+
nttManager.completeOutboundQueuedTransfer(0);
764+
}
765+
766+
function testFuzz_inboundRateLimitShouldQueue(uint256 inboundLimitAmt, uint256 amount) public {
767+
vm.assume(amount > 0 && amount <= type(uint64).max);
768+
vm.assume(inboundLimitAmt < amount);
769+
770+
address user_B = address(0x456);
771+
772+
(DummyTransceiver e1, DummyTransceiver e2) =
773+
TransceiverHelpersLib.setup_transceivers(nttManager);
774+
775+
DummyToken token = DummyToken(nttManager.token());
776+
777+
ITransceiverReceiver[] memory transceivers = new ITransceiverReceiver[](1);
778+
transceivers[0] = e1;
779+
780+
TransceiverStructs.NttManagerMessage memory m;
781+
bytes memory encodedEm;
782+
uint256 inboundLimit = inboundLimitAmt;
783+
TrimmedAmount memory trimmedAmount = TrimmedAmount(uint64(amount), 8);
784+
{
785+
TransceiverStructs.TransceiverMessage memory em;
786+
(m, em) = TransceiverHelpersLib.attestTransceiversHelper(
787+
user_B,
788+
0,
789+
chainId,
790+
nttManager,
791+
nttManager,
792+
trimmedAmount,
793+
// TrimmedAmount(50, 8),
794+
inboundLimit.trim(token.decimals(), token.decimals()),
795+
transceivers
796+
);
797+
encodedEm = TransceiverStructs.encodeTransceiverMessage(
798+
TransceiverHelpersLib.TEST_TRANSCEIVER_PAYLOAD_PREFIX, em
799+
);
800+
}
801+
802+
bytes32 digest =
803+
TransceiverStructs.nttManagerMessageDigest(TransceiverHelpersLib.SENDING_CHAIN_ID, m);
804+
805+
// no quorum yet
806+
assertEq(token.balanceOf(address(user_B)), 0);
807+
808+
vm.expectEmit(address(nttManager));
809+
emit InboundTransferQueued(digest);
810+
e2.receiveMessage(encodedEm);
811+
812+
{
813+
// now we have quorum but it'll hit limit
814+
IRateLimiter.InboundQueuedTransfer memory qt =
815+
nttManager.getInboundQueuedTransfer(digest);
816+
assertEq(qt.amount.getAmount(), trimmedAmount.getAmount());
817+
assertEq(qt.txTimestamp, initialBlockTimestamp);
818+
assertEq(qt.recipient, user_B);
819+
}
820+
821+
// assert that the user doesn't have funds yet
822+
assertEq(token.balanceOf(address(user_B)), 0);
823+
824+
// change block time to (duration - 1) seconds later
825+
uint256 durationElapsedTime = initialBlockTimestamp + nttManager.rateLimitDuration();
826+
vm.warp(durationElapsedTime - 1);
827+
828+
{
829+
// assert that transfer still can't be completed
830+
bytes4 stillQueuedSelector =
831+
bytes4(keccak256("InboundQueuedTransferStillQueued(bytes32,uint256)"));
832+
vm.expectRevert(
833+
abi.encodeWithSelector(stillQueuedSelector, digest, initialBlockTimestamp)
834+
);
835+
nttManager.completeInboundQueuedTransfer(digest);
836+
}
837+
838+
// now complete transfer
839+
vm.warp(durationElapsedTime);
840+
nttManager.completeInboundQueuedTransfer(digest);
841+
842+
{
843+
// assert transfer no longer in queue
844+
bytes4 notQueuedSelector = bytes4(keccak256("InboundQueuedTransferNotFound(bytes32)"));
845+
vm.expectRevert(abi.encodeWithSelector(notQueuedSelector, digest));
846+
nttManager.completeInboundQueuedTransfer(digest);
847+
}
848+
849+
// assert user now has funds
850+
assertEq(
851+
token.balanceOf(address(user_B)),
852+
trimmedAmount.getAmount() * 10 ** (token.decimals() - 8)
853+
);
854+
855+
// replay protection on executeMsg
856+
vm.recordLogs();
857+
nttManager.executeMsg(
858+
TransceiverHelpersLib.SENDING_CHAIN_ID, toWormholeFormat(address(nttManager)), m
859+
);
860+
861+
{
862+
Vm.Log[] memory entries = vm.getRecordedLogs();
863+
assertEq(entries.length, 1);
864+
assertEq(entries[0].topics.length, 3);
865+
assertEq(entries[0].topics[0], keccak256("MessageAlreadyExecuted(bytes32,bytes32)"));
866+
assertEq(entries[0].topics[1], toWormholeFormat(address(nttManager)));
867+
assertEq(
868+
entries[0].topics[2],
869+
TransceiverStructs.nttManagerMessageDigest(
870+
TransceiverHelpersLib.SENDING_CHAIN_ID, m
871+
)
872+
);
873+
}
874+
}
765875
}

0 commit comments

Comments
 (0)