diff --git a/.gitmodules b/.gitmodules index e124719..888d42d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "lib/ds-test"] - path = lib/ds-test - url = https://github.com/dapphub/ds-test +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/Makefile b/Makefile index 5315fa1..2ed0247 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ all :; FOUNDRY_OPTIMIZER=true FOUNDRY_OPTIMIZER_RUNS=200 forge build --use solc:0.8.15 clean :; forge clean -test :; ./test.sh $(match) $(runs) -cov :; rm -rf coverage && mkdir coverage && forge coverage --report lcov && lcov -r lcov.info 'src/test/*' 'src/utils/*' -o lcov.info && genhtml lcov.info --output-directory coverage -certora-con-fee :; certoraRun --solc ~/.solc-select/artifacts/solc-0.8.15/solc-0.8.15 --optimize_map TeleportConstantFee=200 --rule_sanity basic src/TeleportConstantFee.sol --verify TeleportConstantFee:certora/TeleportConstantFee.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output -certora-lin-fee :; certoraRun --solc ~/.solc-select/artifacts/solc-0.8.15/solc-0.8.15 --optimize_map TeleportLinearFee=200 --rule_sanity basic src/TeleportLinearFee.sol --verify TeleportLinearFee:certora/TeleportLinearFee.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output -certora-join :; certoraRun --solc ~/.solc-select/artifacts/solc-0.8.15/solc-0.8.15 --optimize_map TeleportJoin=200,FeesMock=0,Auxiliar=0,VatMock=0,DaiMock=0,DaiJoinMock=0 --rule_sanity basic src/TeleportJoin.sol certora/FeesMock.sol certora/Auxiliar.sol src/test/mocks/VatMock.sol src/test/mocks/DaiMock.sol src/test/mocks/DaiJoinMock.sol --link TeleportJoin:vat=VatMock TeleportJoin:daiJoin=DaiJoinMock DaiJoinMock:vat=VatMock DaiJoinMock:dai=DaiMock --verify TeleportJoin:certora/TeleportJoin.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output --settings -optimisticUnboundedHashing=true,-mediumTimeout=30 -certora-router :; certoraRun --solc ~/.solc-select/artifacts/solc-0.8.15/solc-0.8.15 --optimize_map TeleportRouter=200,TeleportJoinMock=0,DaiMock=0 --rule_sanity basic src/TeleportRouter.sol certora/TeleportJoinMock.sol src/test/mocks/DaiMock.sol --link TeleportRouter:dai=DaiMock --verify TeleportRouter:certora/TeleportRouter.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output -certora-oracle :; certoraRun --solc ~/.solc-select/artifacts/solc-0.8.15/solc-0.8.15 --optimize_map TeleportOracleAuth=200,TeleportJoinMock=0,Auxiliar=0 --rule_sanity basic src/TeleportOracleAuth.sol certora/TeleportJoinMock.sol certora/Auxiliar.sol --link TeleportOracleAuth:teleportJoin=TeleportJoinMock Auxiliar:oracle=TeleportOracleAuth --verify TeleportOracleAuth:certora/TeleportOracleAuth.spec --loop_iter 10 --optimistic_loop $(if $(rule),--rule $(rule),) --multi_assert_check --short_output +test :; ./test.sh $(match) +cov :; rm -rf coverage && mkdir coverage && forge coverage --report lcov && lcov -r lcov.info 'src/test/*' 'src/utils/*' -o lcov.info && genhtml lcov.info --output-directory coverage +certora-con-fee :; PATH=~/.solc-select/artifacts/solc-0.8.15:${PATH} certoraRun --solc_map TeleportConstantFee=solc-0.8.15 --optimize_map TeleportConstantFee=200 --rule_sanity basic src/TeleportConstantFee.sol --verify TeleportConstantFee:certora/TeleportConstantFee.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output +certora-lin-fee :; PATH=~/.solc-select/artifacts/solc-0.8.15:${PATH} certoraRun --solc_map TeleportLinearFee=solc-0.8.15 --optimize_map TeleportLinearFee=200 --rule_sanity basic src/TeleportLinearFee.sol --verify TeleportLinearFee:certora/TeleportLinearFee.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output +certora-join :; PATH=~/.solc-select/artifacts/solc-0.8.15:${PATH} certoraRun --solc_map TeleportJoin=solc-0.8.15,FeesMock=solc-0.8.15,Auxiliar=solc-0.8.15,VatMock=solc-0.8.15,DaiMock=solc-0.8.15,DaiJoinMock=solc-0.8.15 --optimize_map TeleportJoin=200,FeesMock=0,Auxiliar=0,VatMock=0,DaiMock=0,DaiJoinMock=0 --rule_sanity basic src/TeleportJoin.sol certora/FeesMock.sol certora/Auxiliar.sol src/test/mocks/VatMock.sol src/test/mocks/DaiMock.sol src/test/mocks/DaiJoinMock.sol --link TeleportJoin:vat=VatMock TeleportJoin:daiJoin=DaiJoinMock DaiJoinMock:vat=VatMock DaiJoinMock:dai=DaiMock --verify TeleportJoin:certora/TeleportJoin.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output --settings -optimisticUnboundedHashing=true,-mediumTimeout=30 +certora-router :; PATH=~/.solc-select/artifacts/solc-0.8.15:${PATH} certoraRun --solc_map TeleportRouter=solc-0.8.15,GatewayMock=solc-0.8.15,DaiMock=solc-0.8.15,Auxiliar=solc-0.8.15 --optimize_map TeleportRouter=200,GatewayMock=0,DaiMock=0,Auxiliar=0 --rule_sanity basic src/TeleportRouter.sol certora/GatewayMock.sol src/test/mocks/DaiMock.sol certora/Auxiliar.sol --link TeleportRouter:dai=DaiMock --verify TeleportRouter:certora/TeleportRouter.spec $(if $(rule),--rule $(rule),) --multi_assert_check --short_output +certora-oracle :; PATH=~/.solc-select/artifacts/solc-0.8.15:${PATH} certoraRun --solc_map TeleportOracleAuth=solc-0.8.15,TeleportJoinMock=solc-0.8.15,Auxiliar=solc-0.8.15 --optimize_map TeleportOracleAuth=200,TeleportJoinMock=0,Auxiliar=0 --rule_sanity basic src/TeleportOracleAuth.sol certora/TeleportJoinMock.sol certora/Auxiliar.sol --link TeleportOracleAuth:teleportJoin=TeleportJoinMock Auxiliar:oracle=TeleportOracleAuth --verify TeleportOracleAuth:certora/TeleportOracleAuth.spec --loop_iter 10 --optimistic_loop $(if $(rule),--rule $(rule),) --multi_assert_check --short_output diff --git a/certora/Auxiliar.sol b/certora/Auxiliar.sol index d67cb45..78a91f1 100644 --- a/certora/Auxiliar.sol +++ b/certora/Auxiliar.sol @@ -32,6 +32,10 @@ contract Auxiliar { )); } + function addressToBytes32(address addr) external pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } + function bytes32ToAddress(bytes32 addr) external pure returns (address) { return address(uint160(uint256(addr))); } diff --git a/certora/GatewayMock.sol b/certora/GatewayMock.sol new file mode 100644 index 0000000..a1eb75b --- /dev/null +++ b/certora/GatewayMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +pragma solidity 0.8.15; + +import "../src/TeleportGUID.sol"; + +contract GatewayMock { + TeleportGUID public teleportGUID; + bytes32 public sourceDomain; + bytes32 public targetDomain; + uint256 public amount; + + function registerMint( + TeleportGUID memory teleportGUID_ + ) external { + teleportGUID = teleportGUID_; + } + + function settle(bytes32 sourceDomain_, bytes32 targetDomain_, uint256 amount_) external { + sourceDomain = sourceDomain_; + targetDomain = targetDomain_; + amount = amount_; + } +} diff --git a/certora/TeleportJoin.spec b/certora/TeleportJoin.spec index 65ad8ce..3f3e4b8 100644 --- a/certora/TeleportJoin.spec +++ b/certora/TeleportJoin.spec @@ -265,6 +265,62 @@ definition operatorFeeAmt(bool canGenerate, uint256 operatorFee) : 0; +// Verify that registerMint behaves correctly +rule registerMint( + join.TeleportGUID guid + ) { + env e; + + bytes32 hashGUID = aux.getGUIDHash(guid); + + bool blessedBefore; + uint248 pendingBefore; + blessedBefore, pendingBefore = teleports(hashGUID); + + registerMint(e, guid); + + bool blessedAfter; + uint248 pendingAfter; + blessedAfter, pendingAfter = teleports(hashGUID); + + assert(blessedBefore == false, "blessed before call should be false"); + assert(blessedAfter == true, "blessed after call should be true"); + assert(pendingAfter == guid.amount, "pending has not acted as expected"); +} + +// Verify revert rules on registerMint +rule registerMint_revert( + join.TeleportGUID guid + ) { + env e; + + uint256 ward = wards(e.msg.sender); + + bytes32 hashGUID = aux.getGUIDHash(guid); + + bytes32 domain = domain(); + + bool vatLive = vat.live() == 1; + uint256 line = vatLive ? line(guid.sourceDomain): 0; + int256 debt = debt(guid.sourceDomain); + + bool blessed; + uint248 pending; + blessed, pending = teleports(hashGUID); + + registerMint@withrevert(e, guid); + + bool revert1 = e.msg.value > 0; + bool revert2 = ward != 1; + bool revert3 = blessed; + + assert(revert1 => lastReverted, "revert1 failed"); + assert(revert2 => lastReverted, "revert2 failed"); + assert(revert3 => lastReverted, "revert3 failed"); + + assert(lastReverted => revert1 || revert2 || revert3, "Revert rules are not covering all the cases"); +} + // Verify that requestMint behaves correctly rule requestMint( join.TeleportGUID guid, @@ -665,7 +721,7 @@ rule mintPending_revert( revert22 || revert23 || revert24, "Revert rules are not covering all the cases"); } -rule settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) { +rule settle(bytes32 sourceDomain, bytes32 targetDomain, uint256 amount) { env e; bool vatLive = vat.live() == 1; @@ -679,9 +735,9 @@ rule settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) { uint256 vatDaiJoinBefore = vat.dai(currentContract); - uint256 amtToPayBack = batchedDaiToFlush <= artBefore ? batchedDaiToFlush : artBefore; + uint256 amtToPayBack = amount <= artBefore ? amount : artBefore; - settle(e, sourceDomain, batchedDaiToFlush); + settle(e, sourceDomain, targetDomain, amount); int256 debtAfter = debt(sourceDomain); @@ -693,24 +749,26 @@ rule settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) { uint256 vatDaiJoinAfter = vat.dai(currentContract); - assert(to_mathint(debtAfter) == to_mathint(debtBefore) - to_mathint(batchedDaiToFlush), "debt has not decreased as expected"); + assert(to_mathint(debtAfter) == to_mathint(debtBefore) - to_mathint(amount), "debt has not decreased as expected"); assert(vatLive => inkAfter == inkBefore - amtToPayBack, "ink has not decreased as expected"); assert(vatLive => artAfter == artBefore - amtToPayBack, "art has not decreased as expected"); assert(!vatLive => inkAfter == inkBefore, "ink has not stayed the same as expected"); assert(!vatLive => artAfter == artBefore, "art has not stayed the same as expected"); assert(vatLive => cureAfter == (artBefore - amtToPayBack) * RAY(), "cure has not been updated as expected"); assert(!vatLive => cureAfter == cureBefore, "cure has not stayed the same as expected"); - assert(vatLive => vatDaiJoinAfter == vatDaiJoinBefore + (batchedDaiToFlush - amtToPayBack) * RAY(), "join vat dai has not increased as expected 1"); - assert(!vatLive => vatDaiJoinAfter == vatDaiJoinBefore + batchedDaiToFlush * RAY(), "join vat dai has not increased as expected 2"); + assert(vatLive => vatDaiJoinAfter == vatDaiJoinBefore + (amount - amtToPayBack) * RAY(), "join vat dai has not increased as expected 1"); + assert(!vatLive => vatDaiJoinAfter == vatDaiJoinBefore + amount * RAY(), "join vat dai has not increased as expected 2"); } -rule settle_revert(bytes32 sourceDomain, uint256 batchedDaiToFlush) { +rule settle_revert(bytes32 sourceDomain, bytes32 targetDomain, uint256 amount) { env e; require(vat() == vat); require(daiJoin.vat() == vat); require(daiJoin.dai() == dai); + bytes32 domain = domain(); + bool vatLive = vat.live() == 1; int256 debt = debt(sourceDomain); @@ -727,21 +785,22 @@ rule settle_revert(bytes32 sourceDomain, uint256 batchedDaiToFlush) { uint256 daiBalJoin = dai.balanceOf(currentContract); uint256 daiAllJoinDaiJoin = dai.allowance(currentContract, daiJoin); - uint256 amtToPayBack = batchedDaiToFlush <= art ? batchedDaiToFlush : art; + uint256 amtToPayBack = amount <= art ? amount : art; - settle@withrevert(e, sourceDomain, batchedDaiToFlush); + settle@withrevert(e, sourceDomain, targetDomain, amount); bool revert1 = e.msg.value > 0; - bool revert2 = batchedDaiToFlush > max_int256(); - bool revert3 = batchedDaiToFlush * RAY() > max_uint256; - bool revert4 = batchedDaiToFlush * RAY() > vatDaiDaiJoin; - bool revert5 = vatDaiJoin + batchedDaiToFlush * RAY() > max_uint256; - bool revert6 = daiBalJoin < batchedDaiToFlush; - bool revert7 = daiAllJoinDaiJoin < batchedDaiToFlush; - bool revert8 = vatLive && amtToPayBack > 0 && -1 * to_mathint(amtToPayBack) * RAY() < min_int256(); - bool revert9 = vatLive && amtToPayBack > ink; - bool revert10 = vatLive && vatGemJoin + amtToPayBack > max_uint256; - bool revert11 = to_mathint(debt) - to_mathint(batchedDaiToFlush) < min_int256(); + bool revert2 = targetDomain != domain; + bool revert3 = amount > max_int256(); + bool revert4 = amount * RAY() > max_uint256; + bool revert5 = amount * RAY() > vatDaiDaiJoin; + bool revert6 = vatDaiJoin + amount * RAY() > max_uint256; + bool revert7 = daiBalJoin < amount; + bool revert8 = daiAllJoinDaiJoin < amount; + bool revert9 = vatLive && amtToPayBack > 0 && -1 * to_mathint(amtToPayBack) * RAY() < min_int256(); + bool revert10 = vatLive && amtToPayBack > ink; + bool revert11 = vatLive && vatGemJoin + amtToPayBack > max_uint256; + bool revert12 = to_mathint(debt) - to_mathint(amount) < min_int256(); assert(revert1 => lastReverted, "revert1 failed"); assert(revert2 => lastReverted, "revert2 failed"); @@ -754,9 +813,10 @@ rule settle_revert(bytes32 sourceDomain, uint256 batchedDaiToFlush) { assert(revert9 => lastReverted, "revert9 failed"); assert(revert10 => lastReverted, "revert10 failed"); assert(revert11 => lastReverted, "revert11 failed"); + assert(revert12 => lastReverted, "revert12 failed"); assert(lastReverted => revert1 || revert2 || revert3 || revert4 || revert5 || revert6 || revert7 || revert8 || revert9 || - revert10 || revert11, "Revert rules are not covering all the cases"); + revert10 || revert11 || revert12, "Revert rules are not covering all the cases"); } diff --git a/certora/TeleportJoinMock.sol b/certora/TeleportJoinMock.sol index da4d5ad..c2f62a5 100644 --- a/certora/TeleportJoinMock.sol +++ b/certora/TeleportJoinMock.sol @@ -9,8 +9,6 @@ contract TeleportJoinMock { uint256 public operatorFee; uint256 public postFeeAmount; uint256 public totalFee; - bytes32 public sourceDomain; - uint256 public batchedDaiToFlush; function requestMint( TeleportGUID memory teleportGUID_, @@ -23,9 +21,4 @@ contract TeleportJoinMock { postFeeAmount_ = postFeeAmount; totalFee_ = totalFee; } - - function settle(bytes32 sourceDomain_, uint256 batchedDaiToFlush_) external { - sourceDomain = sourceDomain_; - batchedDaiToFlush = batchedDaiToFlush_; - } } diff --git a/certora/TeleportRouter.spec b/certora/TeleportRouter.spec index eb5a4ba..0c2d829 100644 --- a/certora/TeleportRouter.spec +++ b/certora/TeleportRouter.spec @@ -2,30 +2,36 @@ using TeleportRouter as router using DaiMock as dai -using TeleportJoinMock as join +using GatewayMock as gateway +using Auxiliar as aux methods { + batches(bytes32) returns (uint256) envfree + domain() returns (bytes32) envfree domainAt(uint256) returns (bytes32) envfree - domains(address) returns (bytes32) envfree gateways(bytes32) returns (address) envfree hasDomain(bytes32) returns (bool) envfree + fdust() returns (uint256) envfree numDomains() returns (uint256) envfree - requestMint(join.TeleportGUID, uint256, uint256) returns (uint256, uint256) => DISPATCHER(true) - settle(bytes32, uint256) => DISPATCHER(true) + nonce() returns (uint80) envfree + parentDomain() returns (bytes32) envfree + registerMint(gateway.TeleportGUID) => DISPATCHER(true) + settle(bytes32, bytes32, uint256) => DISPATCHER(true) wards(address) returns (uint256) envfree + aux.addressToBytes32(address) returns (bytes32) envfree + aux.bytes32ToAddress(bytes32) returns (address) envfree dai.allowance(address, address) returns (uint256) envfree dai.balanceOf(address) returns (uint256) envfree - join.teleportGUID() returns(bytes32, bytes32, bytes32, bytes32, uint128, uint80, uint48) envfree - join.batchedDaiToFlush() returns (uint256) envfree - join.maxFeePercentage() returns (uint256) envfree - join.operatorFee() returns (uint256) envfree - join.postFeeAmount() returns (uint256) envfree - join.sourceDomain() returns (bytes32) envfree - join.totalFee() returns (uint256) envfree + gateway.teleportGUID() returns(bytes32, bytes32, bytes32, bytes32, uint128, uint80, uint48) envfree + gateway.sourceDomain() returns(bytes32) envfree + gateway.targetDomain() returns(bytes32) envfree + gateway.amount() returns(uint256) envfree } definition RAY() returns uint256 = 10^27; +definition max_uint48() returns mathint = 2^48 - 1; +definition max_uint80() returns mathint = 2^80 - 1; definition min_int256() returns mathint = -1 * 2^255; definition max_int256() returns mathint = 2^255 - 1; @@ -130,10 +136,6 @@ rule file_domain_address(bytes32 what, bytes32 domain, address data) { assert( gateways(domain) == data, "file did not set gateways(domain) as expected" ); - assert( - !dataIsEmpty - => domains(data) == domain, "file did not set domains(gateway) as expected" - ); assert( gatewayWasEmpty && !hasDomainBefore && !dataIsEmpty && numDomainsBefore < max_uint256 => numDomainsAfter == numDomainsBefore + 1, "file did not increase allDomains length as expected" @@ -143,10 +145,6 @@ rule file_domain_address(bytes32 what, bytes32 domain, address data) { !lastReverted && gatewayWasEmpty && !dataIsEmpty => domainAt == domain, "file did not modify allDomains as expected" ); - assert( - !gatewayWasEmpty && gatewayBefore != data - => domains(gatewayBefore) == 0x0000000000000000000000000000000000000000000000000000000000000000, "file did not set domains(gateway) as expected 2" - ); assert( !gatewayWasEmpty && hasDomainBefore && dataIsEmpty => numDomainsAfter == numDomainsBefore - 1, "file did not decrease allDomains length as expected" @@ -160,8 +158,7 @@ rule file_domain_address_revert(bytes32 what, bytes32 domain, address data) { bool whatIsGateway = what == 0x6761746577617900000000000000000000000000000000000000000000000000; bool dataIsEmpty = data == 0x0000000000000000000000000000000000000000; uint256 ward = wards(e.msg.sender); - address gateway = gateways(domain); - bool gatewayWasEmpty = gateway == 0x0000000000000000000000000000000000000000; + bool gatewayWasEmpty = gateways(domain) == 0x0000000000000000000000000000000000000000; uint256 numDomains = numDomains(); bool hasDomain = hasDomain(domain); uint256 pos = indexesGhost(domain); @@ -186,19 +183,42 @@ rule file_domain_address_revert(bytes32 what, bytes32 domain, address data) { revert4 || revert5, "Revert rules are not covering all the cases"); } -// Verify that requestMint behaves correctly -rule requestMint( - router.TeleportGUID guid, - uint256 maxFeePercentage, - uint256 operatorFee - ) { +// Verify that fdust behaves correctly on file +rule file_uint256(bytes32 what, uint256 data) { + env e; + + file(e, what, data); + + assert(fdust() == data, "file did not set fdust as expected"); +} + + +// Verify revert rules on file +rule file_uint256_revert(bytes32 what, uint256 data) { + env e; + + uint256 ward = wards(e.msg.sender); + + file@withrevert(e, what, data); + + bool revert1 = e.msg.value > 0; + bool revert2 = ward != 1; + bool revert3 = what != 0x6664757374000000000000000000000000000000000000000000000000000000; + + assert(revert1 => lastReverted, "revert1 failed"); + assert(revert2 => lastReverted, "revert2 failed"); + assert(revert3 => lastReverted, "revert3 failed"); + + assert(lastReverted => revert1 || revert2 || revert3, "Revert rules are not covering all the cases"); +} + +// Verify that registerMint behaves correctly +rule registerMint(router.TeleportGUID guid) { env e; - require(gateways(guid.targetDomain) == join); + require(gateways(guid.targetDomain) == gateway); - uint256 postFeeAmount; - uint256 totalFee; - postFeeAmount, totalFee = requestMint(e, guid, maxFeePercentage, operatorFee); + registerMint(e, guid); bytes32 sourceDomain; bytes32 targetDomain; @@ -207,7 +227,7 @@ rule requestMint( uint128 amount; uint80 nonce; uint48 timestamp; - sourceDomain, targetDomain, receiver, operator, amount, nonce, timestamp = join.teleportGUID(); + sourceDomain, targetDomain, receiver, operator, amount, nonce, timestamp = gateway.teleportGUID(); assert(sourceDomain == guid.sourceDomain, "guid.sourceDomain was not preserved"); assert(targetDomain == guid.targetDomain, "guid.targetDomain was not preserved"); assert(receiver == guid.receiver, "guid.receiver was not preserved"); @@ -215,30 +235,24 @@ rule requestMint( assert(amount == guid.amount, "guid.amount was not preserved"); assert(nonce == guid.nonce, "guid.nonce was not preserved"); assert(timestamp == guid.timestamp, "guid.timestamp was not preserved"); - assert(join.maxFeePercentage() == maxFeePercentage, "maxFeePercentage was not preserved"); - assert(join.operatorFee() == operatorFee, "operatorFee was not preserved"); - assert(join.postFeeAmount() == postFeeAmount, "postFeeAmount was not preserved"); - assert(join.totalFee() == totalFee, "totalFee was not preserved"); } -// Verify revert rules on requestMint -rule requestMint_revert( - router.TeleportGUID guid, - uint256 maxFeePercentage, - uint256 operatorFee - ) { +// Verify revert rules on registerMint +rule registerMint_revert(router.TeleportGUID guid) { env e; - address targetGateway = gateways(guid.targetDomain); - require(targetGateway == join); - require(currentContract != targetGateway); - + address parentGateway = gateways(parentDomain()); address sourceGateway = gateways(guid.sourceDomain); + address targetGateway = gateways(guid.targetDomain) != 0x0000000000000000000000000000000000000000 + ? gateways(guid.targetDomain) + : parentGateway; + require(targetGateway == gateway); + require(currentContract != targetGateway); - requestMint@withrevert(e, guid, maxFeePercentage, operatorFee); + registerMint@withrevert(e, guid); bool revert1 = e.msg.value > 0; - bool revert2 = sourceGateway != e.msg.sender; + bool revert2 = parentGateway != e.msg.sender && sourceGateway != e.msg.sender; bool revert3 = targetGateway == 0x0000000000000000000000000000000000000000; assert(revert1 => lastReverted, "revert1 failed"); @@ -249,49 +263,54 @@ rule requestMint_revert( } // Verify that settle behaves correctly -rule settle(bytes32 targetDomain, uint256 batchedDaiToFlush) { +rule settle(bytes32 sourceDomain, bytes32 targetDomain, uint256 amount) { env e; - address targetGateway = gateways(targetDomain); - - require(targetGateway == join); + address targetGateway = gateways(targetDomain) != 0x0000000000000000000000000000000000000000 + ? gateways(targetDomain) + : gateways(parentDomain()); + require(targetGateway == gateway); require(currentContract != targetGateway); uint256 daiSenderBefore = dai.balanceOf(e.msg.sender); uint256 daiGatewayBefore = dai.balanceOf(targetGateway); - settle(e, targetDomain, batchedDaiToFlush); + settle(e, sourceDomain, targetDomain, amount); uint256 daiSenderAfter = dai.balanceOf(e.msg.sender); uint256 daiGatewayAfter = dai.balanceOf(targetGateway); - assert(join.sourceDomain() == domains(e.msg.sender), "sourceDomain was not preserved"); - assert(join.batchedDaiToFlush() == batchedDaiToFlush, "batchedDaiToFlush was not preserved"); - assert(e.msg.sender != targetGateway => daiSenderAfter == daiSenderBefore - batchedDaiToFlush, "Sender's DAI balance did not decrease as expected"); - assert(e.msg.sender != targetGateway => daiGatewayAfter == daiGatewayBefore + batchedDaiToFlush, "Gateway's DAI balance did not increase as expected"); + assert(gateway.sourceDomain() == sourceDomain, "sourceDomain was not preserved"); + assert(gateway.targetDomain() == targetDomain, "targetDomain was not preserved"); + assert(gateway.amount() == amount, "amount was not preserved"); + assert(e.msg.sender != targetGateway => daiSenderAfter == daiSenderBefore - amount, "Sender's DAI balance did not decrease as expected"); + assert(e.msg.sender != targetGateway => daiGatewayAfter == daiGatewayBefore + amount, "Gateway's DAI balance did not increase as expected"); } // Verify revert rules on settle -rule settle_revert(bytes32 targetDomain, uint256 batchedDaiToFlush) { +rule settle_revert(bytes32 sourceDomain, bytes32 targetDomain, uint256 amount) { env e; - address targetGateway = gateways(targetDomain); - require(targetGateway == join); + address parentGateway = gateways(parentDomain()); + address sourceGateway = gateways(sourceDomain); + address targetGateway = gateways(targetDomain) != 0x0000000000000000000000000000000000000000 + ? gateways(targetDomain) + : parentGateway; + require(targetGateway == gateway); require(currentContract != targetGateway); - bytes32 sourceDomain = domains(e.msg.sender); uint256 allowance = dai.allowance(e.msg.sender, currentContract); uint256 daiSender = dai.balanceOf(e.msg.sender); uint256 daiTarget = dai.balanceOf(targetGateway); - settle@withrevert(e, targetDomain, batchedDaiToFlush); + settle@withrevert(e, sourceDomain, targetDomain, amount); bool revert1 = e.msg.value > 0; - bool revert2 = sourceDomain == 0x0000000000000000000000000000000000000000000000000000000000000000; + bool revert2 = parentGateway != e.msg.sender && sourceGateway != e.msg.sender; bool revert3 = targetGateway == 0x0000000000000000000000000000000000000000; - bool revert4 = e.msg.sender != currentContract && allowance < batchedDaiToFlush; - bool revert5 = daiSender < batchedDaiToFlush; - bool revert6 = e.msg.sender != targetGateway && daiTarget + batchedDaiToFlush > max_uint256; + bool revert4 = e.msg.sender != currentContract && allowance < amount; + bool revert5 = daiSender < amount; + bool revert6 = e.msg.sender != targetGateway && daiTarget + amount > max_uint256; assert(revert1 => lastReverted, "revert1 failed"); assert(revert2 => lastReverted, "revert2 failed"); @@ -303,3 +322,187 @@ rule settle_revert(bytes32 targetDomain, uint256 batchedDaiToFlush) { assert(lastReverted => revert1 || revert2 || revert3 || revert4 || revert5 || revert6, "Revert rules are not covering all the cases"); } + +// Verify that initiateTeleport behaves correctly +rule initiateTeleport( + bytes32 _targetDomain, + bytes32 _receiver, + uint128 _amount, + bytes32 _operator + ) { + env e; + + uint256 option; + + require(e.block.timestamp <= max_uint48()); + + bytes32 domain = domain(); + + uint256 batchesTargetBefore = batches(_targetDomain); + uint80 nonceBefore = nonce(); + uint256 daiSenderBefore = dai.balanceOf(e.msg.sender); + uint256 daiRouterBefore = dai.balanceOf(currentContract); + + require(gateways(_targetDomain) == gateway); + + if (option == 0) { + initiateTeleport(e, _targetDomain, _receiver, _amount, _operator); + } else if (option == 1) { + initiateTeleport(e, _targetDomain, aux.bytes32ToAddress(_receiver), _amount, aux.bytes32ToAddress(_operator)); + } else { + initiateTeleport(e, _targetDomain, aux.bytes32ToAddress(_receiver), _amount); + } + + uint256 batchesTargetAfter = batches(_targetDomain); + uint80 nonceAfter = nonce(); + uint256 daiSenderAfter = dai.balanceOf(e.msg.sender); + uint256 daiRouterAfter = dai.balanceOf(currentContract); + + bytes32 sourceDomain; + bytes32 targetDomain; + bytes32 receiver; + bytes32 operator; + uint128 amount; + uint80 nonce; + uint48 timestamp; + sourceDomain, targetDomain, receiver, operator, amount, nonce, timestamp = gateway.teleportGUID(); + + assert(batchesTargetAfter == batchesTargetBefore + amount, "batchesTarget did not increase as expected"); + assert(nonceAfter == nonceBefore + 1, "nonce did not increase as expected"); + assert(e.msg.sender != currentContract => daiSenderAfter == daiSenderBefore - amount, "Sender's DAI balance did not decrease as expected"); + assert(e.msg.sender != currentContract => daiRouterAfter == daiRouterBefore + amount, "Router's DAI balance did not increase as expected"); + assert(sourceDomain == domain, "sourceDomain was not preserved"); + assert(targetDomain == _targetDomain, "targetDomain was not preserved"); + if (option == 0) { + assert(receiver == _receiver, "receiver was not preserved in option 1"); + assert(operator == _operator, "operator was not preserved in option 1"); + } else if (option == 1) { + assert(receiver == aux.addressToBytes32(aux.bytes32ToAddress(_receiver)), "receiver was not preserved in option 2"); + assert(operator == aux.addressToBytes32(aux.bytes32ToAddress(_operator)), "operator was not preserved in option 2"); + } else { + assert(receiver == aux.addressToBytes32(aux.bytes32ToAddress(_receiver)), "receiver was not preserved in option 3"); + assert(operator == 0x0000000000000000000000000000000000000000000000000000000000000000, "operator was not preserved in option 3"); + } + assert(amount == _amount, "amount was not preserved"); + assert(nonce == nonceBefore, "nonce was not preserved"); + assert(timestamp == e.block.timestamp, "timestamp was not preserved"); +} + +// Verify revert rules on initiateTeleport +rule initiateTeleport_revert( + bytes32 _targetDomain, + bytes32 _receiver, + uint128 _amount, + bytes32 _operator + ) { + env e; + + uint256 option; + + address targetGateway = gateways(_targetDomain) != 0x0000000000000000000000000000000000000000 + ? gateways(_targetDomain) + : gateways(parentDomain()); + require(targetGateway == gateway); + require(currentContract != targetGateway); + + uint256 batchesTarget = batches(_targetDomain); + uint80 nonce = nonce(); + uint256 allowance = dai.allowance(e.msg.sender, currentContract); + uint256 daiSender = dai.balanceOf(e.msg.sender); + uint256 daiRouter = dai.balanceOf(currentContract); + + if (option == 0) { + initiateTeleport@withrevert(e, _targetDomain, _receiver, _amount, _operator); + } else if (option == 1) { + initiateTeleport@withrevert(e, _targetDomain, aux.bytes32ToAddress(_receiver), _amount, aux.bytes32ToAddress(_operator)); + } else { + initiateTeleport@withrevert(e, _targetDomain, aux.bytes32ToAddress(_receiver), _amount); + } + + bool revert1 = e.msg.value > 0; + bool revert2 = nonce + 1 > max_uint80(); + bool revert3 = batchesTarget + to_uint256(_amount) > max_uint256; + bool revert4 = e.msg.sender != currentContract && allowance < _amount; + bool revert5 = daiSender < _amount; + bool revert6 = e.msg.sender != currentContract && daiRouter + _amount > max_uint256; + bool revert7 = targetGateway == 0x0000000000000000000000000000000000000000; + + assert(revert1 => lastReverted, "revert1 failed"); + assert(revert2 => lastReverted, "revert2 failed"); + assert(revert3 => lastReverted, "revert3 failed"); + assert(revert4 => lastReverted, "revert4 failed"); + assert(revert5 => lastReverted, "revert5 failed"); + assert(revert6 => lastReverted, "revert6 failed"); + assert(revert7 => lastReverted, "revert7 failed"); + + assert(lastReverted => revert1 || revert2 || revert3 || + revert4 || revert5 || revert6 || + revert7, "Revert rules are not covering all the cases"); +} + +// Verify that flush behaves correctly +rule flush(bytes32 _targetDomain) { + env e; + + address targetGateway = gateways(_targetDomain) != 0x0000000000000000000000000000000000000000 + ? gateways(_targetDomain) + : gateways(parentDomain()); + + bytes32 domain = domain(); + + uint256 batchesTargetBefore = batches(_targetDomain); + uint256 daiRouterBefore = dai.balanceOf(currentContract); + uint256 daiTargetBefore = dai.balanceOf(targetGateway); + + require(gateways(_targetDomain) == gateway); + + flush(e, _targetDomain); + + uint256 batchesTargetAfter = batches(_targetDomain); + uint256 daiRouterAfter = dai.balanceOf(currentContract); + uint256 daiTargetAfter = dai.balanceOf(targetGateway); + + bytes32 sourceDomain = gateway.sourceDomain(); + bytes32 targetDomain = gateway.targetDomain(); + uint256 amount = gateway.amount(); + + assert(batchesTargetAfter == 0, "batchesTarget did not decrease to 0"); + assert(targetGateway != currentContract => daiRouterAfter == daiRouterBefore - amount, "Router's DAI balance did not decrease as expected"); + assert(targetGateway != currentContract => daiTargetAfter == daiTargetBefore + amount, "Target's DAI balance did not increase as expected"); + assert(sourceDomain == domain, "sourceDomain was not preserved"); + assert(targetDomain == _targetDomain, "targetDomain was not preserved"); + assert(amount == batchesTargetBefore, "amount was not preserved"); +} + +// Verify revert rules on flush +rule flush_revert(bytes32 _targetDomain) { + env e; + + address targetGateway = gateways(_targetDomain) != 0x0000000000000000000000000000000000000000 + ? gateways(_targetDomain) + : gateways(parentDomain()); + require(targetGateway == gateway); + require(currentContract != targetGateway); + + uint256 batchesTarget = batches(_targetDomain); + uint256 fdust = fdust(); + uint256 daiRouter = dai.balanceOf(currentContract); + uint256 daiTarget = dai.balanceOf(targetGateway); + + flush@withrevert(e, _targetDomain); + + bool revert1 = e.msg.value > 0; + bool revert2 = batchesTarget < fdust; + bool revert3 = targetGateway == 0x0000000000000000000000000000000000000000; + bool revert4 = daiRouter < batchesTarget; + bool revert5 = targetGateway != currentContract && daiTarget + batchesTarget > max_uint256; + + assert(revert1 => lastReverted, "revert1 failed"); + assert(revert2 => lastReverted, "revert2 failed"); + assert(revert3 => lastReverted, "revert3 failed"); + assert(revert4 => lastReverted, "revert4 failed"); + assert(revert5 => lastReverted, "revert5 failed"); + + assert(lastReverted => revert1 || revert2 || revert3 || + revert4 || revert5, "Revert rules are not covering all the cases"); +} diff --git a/lib/ds-test b/lib/ds-test deleted file mode 160000 index 0a5da56..0000000 --- a/lib/ds-test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a5da56b0d65960e6a994d2ec8245e6edd38c248 diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..405727d --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 405727dc7580aaaa6e5b626638dd37ff171beae3 diff --git a/src/TeleportJoin.sol b/src/TeleportJoin.sol index f0351c6..d5841b9 100644 --- a/src/TeleportJoin.sol +++ b/src/TeleportJoin.sol @@ -72,7 +72,7 @@ contract TeleportJoin { event Mint( bytes32 indexed hashGUID, TeleportGUID teleportGUID, uint256 amount, uint256 maxFeePercentage, uint256 operatorFee, address originator ); - event Settle(bytes32 indexed sourceDomain, uint256 batchedDaiToFlush); + event Settle(bytes32 indexed sourceDomain, uint256 amount); struct TeleportStatus { bool blessed; @@ -144,6 +144,21 @@ contract TeleportJoin { cure_ = art * RAY; } + /** + * @dev Internal function that registers a teleport + * @param teleportGUID Struct which contains the whole teleport data + * @param hashGUID Hash of the prev struct + **/ + function _register( + TeleportGUID calldata teleportGUID, + bytes32 hashGUID + ) internal { + require(!teleports[hashGUID].blessed, "TeleportJoin/already-blessed"); + teleports[hashGUID].blessed = true; + teleports[hashGUID].pending = teleportGUID.amount; + emit Register(hashGUID, teleportGUID); + } + /** * @dev Internal function that executes the mint after a teleport is registered * @param teleportGUID Struct which contains the whole teleport data @@ -211,6 +226,16 @@ contract TeleportJoin { emit Mint(hashGUID, teleportGUID, amtToTake, maxFeePercentage, operatorFee, msg.sender); } + /** + * @dev External authed function that registers the teleport + * @param teleportGUID Struct which contains the whole teleport data + **/ + function registerMint( + TeleportGUID calldata teleportGUID + ) external auth { + _register(teleportGUID, getGUIDHash(teleportGUID)); + } + /** * @dev External authed function that registers the teleport and executes the mint after * @param teleportGUID Struct which contains the whole teleport data @@ -225,10 +250,7 @@ contract TeleportJoin { uint256 operatorFee ) external auth returns (uint256 postFeeAmount, uint256 totalFee) { bytes32 hashGUID = getGUIDHash(teleportGUID); - require(!teleports[hashGUID].blessed, "TeleportJoin/already-blessed"); - teleports[hashGUID].blessed = true; - teleports[hashGUID].pending = teleportGUID.amount; - emit Register(hashGUID, teleportGUID); + _register(teleportGUID, hashGUID); (postFeeAmount, totalFee) = _mint(teleportGUID, hashGUID, maxFeePercentage, operatorFee); } @@ -253,21 +275,23 @@ contract TeleportJoin { /** * @dev External function that repays debt with DAI previously pushed to this contract (in general coming from the bridges) * @param sourceDomain domain where the DAI is coming from - * @param batchedDaiToFlush Amount of DAI that is being processed for repayment + * @param targetDomain this domain + * @param amount Amount of DAI that is being processed for repayment **/ - function settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) external { - require(batchedDaiToFlush <= uint256(type(int256).max), "TeleportJoin/overflow"); - daiJoin.join(address(this), batchedDaiToFlush); + function settle(bytes32 sourceDomain, bytes32 targetDomain, uint256 amount) external { + require(targetDomain == domain, "TeleportJoin/incorrect-targetDomain"); + require(amount <= uint256(type(int256).max), "TeleportJoin/overflow"); + daiJoin.join(address(this), amount); if (vat.live() == 1) { (, uint256 art_) = vat.urns(ilk, address(this)); // rate == RAY => normalized debt == actual debt - uint256 amtToPayBack = _min(batchedDaiToFlush, art_); + uint256 amtToPayBack = _min(amount, art_); vat.frob(ilk, address(this), address(this), address(this), -int256(amtToPayBack), -int256(amtToPayBack)); vat.slip(ilk, address(this), -int256(amtToPayBack)); unchecked { art = art_ - amtToPayBack; // Always safe operation } } - debt[sourceDomain] -= int256(batchedDaiToFlush); - emit Settle(sourceDomain, batchedDaiToFlush); + debt[sourceDomain] -= int256(amount); + emit Settle(sourceDomain, amount); } } diff --git a/src/TeleportRouter.sol b/src/TeleportRouter.sol index 41f4fa6..628d763 100644 --- a/src/TeleportRouter.sol +++ b/src/TeleportRouter.sol @@ -20,16 +20,13 @@ import "./TeleportGUID.sol"; import "./utils/EnumerableSet.sol"; interface TokenLike { - function transferFrom(address _from, address _to, uint256 _value) external returns (bool success); + function approve(address, uint256) external returns (bool); + function transferFrom(address, address, uint256) external returns (bool); } interface GatewayLike { - function requestMint( - TeleportGUID calldata teleportGUID, - uint256 maxFeePercentage, - uint256 operatorFee - ) external returns (uint256 postFeeAmount, uint256 totalFee); - function settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) external; + function registerMint(TeleportGUID calldata) external; + function settle(bytes32, bytes32, uint256) external; } contract TeleportRouter { @@ -38,23 +35,32 @@ contract TeleportRouter { mapping (address => uint256) public wards; // Auth mapping (bytes32 => address) public gateways; // GatewayLike contracts called by the router for each domain - mapping (address => bytes32) public domains; // Domains for each gateway + mapping (bytes32 => uint256) public batches; // Pending DAI to flush per target domain EnumerableSet.Bytes32Set private allDomains; + uint80 public nonce; + uint256 public fdust; // The minimum amount of DAI to be flushed per target domain (prevent spam) - TokenLike immutable public dai; // L1 DAI ERC20 token + TokenLike immutable public dai; + bytes32 immutable public domain; + bytes32 immutable public parentDomain; event Rely(address indexed usr); event Deny(address indexed usr); event File(bytes32 indexed what, bytes32 indexed domain, address data); + event File(bytes32 indexed what, uint256 data); + event InitiateTeleport(TeleportGUID teleport); + event Flush(bytes32 indexed targetDomain, uint256 dai); modifier auth { require(wards[msg.sender] == 1, "TeleportRouter/not-authorized"); _; } - constructor(address dai_) { + constructor(address dai_, bytes32 domain_, bytes32 parentDomain_) { dai = TokenLike(dai_); + domain = domain_; + parentDomain = parentDomain_; wards[msg.sender] = 1; emit Rely(msg.sender); } @@ -72,41 +78,46 @@ contract TeleportRouter { /** * @notice Allows auth to configure the router. The only supported operation is "gateway", * which allows adding, replacing or removing a gateway contract for a given domain. The router forwards `settle()` - * and `requestMint()` calls to the gateway contract installed for a given domain. Gateway contracts must therefore - * conform to the GatewayLike interface. Examples of valid gateways include TeleportJoin (for the L1 domain) - * and L1 bridge contracts (for L2 domains). - * @dev In addition to updating the mapping `gateways` which maps GatewayLike contracts to domain names and - * the reverse mapping `domains` which maps domain names to GatewayLike contracts, this method also maintains - * the enumerable set `allDomains`. + * and `registerMint()` calls to the gateway contract installed for a given domain. Gateway contracts must therefore + * conform to the GatewayLike interface. Examples of valid gateways include TeleportJoin (for the domain of the TeleportRouter instance), + * DomainHost bridges (for the domains that are children to TeleportRouter's domain) + * and a DomainGuest bridge (for the domain that is parent to TeleportRouter's domain). + * @dev In addition to updating the mapping `gateways` which maps GatewayLike contracts to domain names this method + * also maintains the enumerable set `allDomains`. * @param what The name of the operation. Only "gateway" is supported. - * @param domain The domain for which a GatewayLike contract is added, replaced or removed. + * @param _domain The domain for which a GatewayLike contract is added, replaced or removed. * @param data The address of the GatewayLike contract to install for the domain (or address(0) to remove a domain) */ - function file(bytes32 what, bytes32 domain, address data) external auth { + function file(bytes32 what, bytes32 _domain, address data) external auth { if (what == "gateway") { - address prevGateway = gateways[domain]; - if(prevGateway == address(0)) { + address prevGateway = gateways[_domain]; + if (prevGateway == address(0)) { // new domain => add it to allDomains - if(data != address(0)) { - allDomains.add(domain); + if (data != address(0)) { + allDomains.add(_domain); } } else { - // existing domain - domains[prevGateway] = bytes32(0); - if(data == address(0)) { + // existing domain + if (data == address(0)) { // => remove domain from allDomains - allDomains.remove(domain); + allDomains.remove(_domain); } } - gateways[domain] = data; - if(data != address(0)) { - domains[data] = domain; - } + gateways[_domain] = data; } else { revert("TeleportRouter/file-unrecognized-param"); } - emit File(what, domain, data); + emit File(what, _domain, data); + } + + function file(bytes32 what, uint256 data) external auth { + if (what == "fdust") { + fdust = data; + } else { + revert("TeleportRouter/file-unrecognized-param"); + } + emit File(what, data); } function numDomains() external view returns (uint256) { @@ -115,42 +126,144 @@ contract TeleportRouter { function domainAt(uint256 index) external view returns (bytes32) { return allDomains.at(index); } - function hasDomain(bytes32 domain) external view returns (bool) { - return allDomains.contains(domain); + function hasDomain(bytes32 _domain) external view returns (bool) { + return allDomains.contains(_domain); } /** - * @notice Call a GatewayLike contract to request the minting of DAI. The sender must be a supported gateway + * @notice Call a GatewayLike contract to register the minting of DAI. The sender must be a supported gateway * @param teleportGUID The teleport GUID to register - * @param maxFeePercentage Max percentage of the withdrawn amount (in WAD) to be paid as fee (e.g 1% = 0.01 * WAD) - * @param operatorFee The amount of DAI to pay to the operator - * @return postFeeAmount The amount of DAI sent to the receiver after taking out fees - * @return totalFee The total amount of DAI charged as fees */ - function requestMint( - TeleportGUID calldata teleportGUID, - uint256 maxFeePercentage, - uint256 operatorFee - ) external returns (uint256 postFeeAmount, uint256 totalFee) { - require(msg.sender == gateways[teleportGUID.sourceDomain], "TeleportRouter/sender-not-gateway"); + function registerMint(TeleportGUID calldata teleportGUID) external { + // We trust the parent gateway with any sourceDomain as a compromised parent gateway implies compromised child + // Otherwise we restrict passing messages only from the actual source domain + require(msg.sender == gateways[parentDomain] || msg.sender == gateways[teleportGUID.sourceDomain], "TeleportRouter/sender-not-gateway"); + + _registerMint(teleportGUID); + } + + function _registerMint(TeleportGUID memory teleportGUID) internal { address gateway = gateways[teleportGUID.targetDomain]; + // Use fallback if no gateway is configured for the target domain + if (gateway == address(0)) gateway = gateways[parentDomain]; require(gateway != address(0), "TeleportRouter/unsupported-target-domain"); - (postFeeAmount, totalFee) = GatewayLike(gateway).requestMint(teleportGUID, maxFeePercentage, operatorFee); + GatewayLike(gateway).registerMint(teleportGUID); } /** * @notice Call a GatewayLike contract to settle a batch of sourceDomain -> targetDomain DAI transfer. * The sender must be a supported gateway - * @param targetDomain The domain receiving the batch of DAI (only L1 supported for now) - * @param batchedDaiToFlush The amount of DAI in the batch + * @param sourceDomain The domain sending the batch of DAI + * @param targetDomain The domain receiving the batch of DAI + * @param amount The amount of DAI in the batch */ - function settle(bytes32 targetDomain, uint256 batchedDaiToFlush) external { - bytes32 sourceDomain = domains[msg.sender]; - require(sourceDomain != bytes32(0), "TeleportRouter/sender-not-gateway"); + function settle(bytes32 sourceDomain, bytes32 targetDomain, uint256 amount) external { + // We trust the parent gateway with any sourceDomain as a compromised parent gateway implies compromised child + // Otherwise we restrict passing messages only from the actual source domain + require(msg.sender == gateways[parentDomain] || msg.sender == gateways[sourceDomain], "TeleportRouter/sender-not-gateway"); + + _settle(msg.sender, sourceDomain, targetDomain, amount); + } + + function _settle(address from, bytes32 sourceDomain, bytes32 targetDomain, uint256 amount) internal { address gateway = gateways[targetDomain]; + // Use fallback if no gateway is configured for the target domain + if (gateway == address(0)) gateway = gateways[parentDomain]; require(gateway != address(0), "TeleportRouter/unsupported-target-domain"); - // Forward the DAI to settle to the gateway contract - dai.transferFrom(msg.sender, gateway, batchedDaiToFlush); - GatewayLike(gateway).settle(sourceDomain, batchedDaiToFlush); + // Forward the DAI to settle to the gateway contract + dai.transferFrom(from, gateway, amount); + GatewayLike(gateway).settle(sourceDomain, targetDomain, amount); + } + + /** + * @notice Initiate Maker teleport + * @dev Will fire a teleport event, burn the dai and initiate a censorship-resistant slow-path message + * @param targetDomain The target domain to teleport to + * @param receiver The receiver address of the DAI on the target domain + * @param amount The amount of DAI to teleport + **/ + function initiateTeleport( + bytes32 targetDomain, + address receiver, + uint128 amount + ) external { + initiateTeleport( + targetDomain, + addressToBytes32(receiver), + amount, + 0 + ); + } + + /** + * @notice Initiate Maker teleport + * @dev Will fire a teleport event, burn the dai and initiate a censorship-resistant slow-path message + * @param targetDomain The target domain to teleport to + * @param receiver The receiver address of the DAI on the target domain + * @param amount The amount of DAI to teleport + * @param operator An optional address that can be used to mint the DAI at the destination domain (useful for automated relays) + **/ + function initiateTeleport( + bytes32 targetDomain, + address receiver, + uint128 amount, + address operator + ) external { + initiateTeleport( + targetDomain, + addressToBytes32(receiver), + amount, + addressToBytes32(operator) + ); + } + + /** + * @notice Initiate Maker teleport + * @dev Will fire a teleport event, burn the dai and initiate a censorship-resistant slow-path message + * @param targetDomain The target domain to teleport to + * @param receiver The receiver address of the DAI on the target domain + * @param amount The amount of DAI to teleport + * @param operator An optional address that can be used to mint the DAI at the destination domain (useful for automated relays) + **/ + function initiateTeleport( + bytes32 targetDomain, + bytes32 receiver, + uint128 amount, + bytes32 operator + ) public { + TeleportGUID memory teleport = TeleportGUID({ + sourceDomain: domain, + targetDomain: targetDomain, + receiver: receiver, + operator: operator, + amount: amount, + nonce: nonce++, + timestamp: uint48(block.timestamp) + }); + + batches[targetDomain] += amount; + require(dai.transferFrom(msg.sender, address(this), amount), "TeleportRouter/transfer-failed"); + + // Initiate the censorship-resistant slow-path + _registerMint(teleport); + + // Oracle listens to this event for the fast-path + emit InitiateTeleport(teleport); + } + + /** + * @notice Flush batched DAI to the target domain + * @dev Will initiate a settle operation along the secure, slow routing path + * @param targetDomain The target domain to settle + **/ + function flush(bytes32 targetDomain) external { + uint256 daiToFlush = batches[targetDomain]; + require(daiToFlush >= fdust, "TeleportRouter/flush-dust"); + + batches[targetDomain] = 0; + + _settle(address(this), domain, targetDomain, daiToFlush); + + emit Flush(targetDomain, daiToFlush); } } diff --git a/src/test/TeleportConstantFee.t.sol b/src/test/TeleportConstantFee.t.sol index 78e06d9..fa6f533 100644 --- a/src/test/TeleportConstantFee.t.sol +++ b/src/test/TeleportConstantFee.t.sol @@ -16,7 +16,7 @@ pragma solidity 0.8.15; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import "src/TeleportConstantFee.sol"; import "src/TeleportGUID.sol"; @@ -25,9 +25,7 @@ interface Hevm { function warp(uint) external; } -contract TeleportConstantFeeTest is DSTest { - - Hevm internal hevm = Hevm(HEVM_ADDRESS); +contract TeleportConstantFeeTest is Test { uint256 internal fee = 1 ether / 100; uint256 internal ttl = 8 days; @@ -94,7 +92,7 @@ contract TeleportConstantFeeTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - hevm.warp(block.timestamp + ttl); + vm.warp(block.timestamp + ttl); assertEq(teleportConstantFee.getFee(guid, 0, 0, 0, 100 ether), 0); } diff --git a/src/test/TeleportJoin.t.sol b/src/test/TeleportJoin.t.sol index 46f20f8..20fa23a 100644 --- a/src/test/TeleportJoin.t.sol +++ b/src/test/TeleportJoin.t.sol @@ -16,7 +16,7 @@ pragma solidity 0.8.15; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import "src/TeleportJoin.sol"; import "src/TeleportConstantFee.sol"; @@ -32,9 +32,7 @@ interface Hevm { function store(address, bytes32, bytes32) external; } -contract TeleportJoinTest is DSTest { - - Hevm internal hevm = Hevm(HEVM_ADDRESS); +contract TeleportJoinTest is Test { bytes32 constant internal ilk = "L2DAI"; bytes32 constant internal domain = "ethereum"; TeleportJoin internal join; @@ -74,52 +72,6 @@ contract TeleportJoinTest is DSTest { (, pending_) = join.teleports(getGUIDHash(guid)); } - function _tryRely(address usr) internal returns (bool ok) { - (ok,) = address(join).call(abi.encodeWithSignature("rely(address)", usr)); - } - - function _tryDeny(address usr) internal returns (bool ok) { - (ok,) = address(join).call(abi.encodeWithSignature("deny(address)", usr)); - } - - function _tryFile(bytes32 what, address data) internal returns (bool ok) { - (ok,) = address(join).call(abi.encodeWithSignature("file(bytes32,address)", what, data)); - } - - function _tryFile(bytes32 what, bytes32 domain_, address data) internal returns (bool ok) { - (ok,) = address(join).call(abi.encodeWithSignature("file(bytes32,bytes32,address)", what, domain_, data)); - } - - function _tryFile(bytes32 what, bytes32 domain_, uint256 data) internal returns (bool ok) { - (ok,) = address(join).call(abi.encodeWithSignature("file(bytes32,bytes32,uint256)", what, domain_, data)); - } - - function _tryRequestMint( - TeleportGUID memory teleportGUID, - uint256 maxFeePercentage, - uint256 operatorFee - ) internal returns (bool ok) { - (ok,) = address(join).call(abi.encodeWithSignature( - "requestMint((bytes32,bytes32,bytes32,bytes32,uint128,uint80,uint48),uint256,uint256)", - teleportGUID, - maxFeePercentage, - operatorFee - )); - } - - function _tryMintPending( - TeleportGUID memory teleportGUID, - uint256 maxFeePercentage, - uint256 operatorFee - ) internal returns (bool ok) { - (ok,) = address(join).call(abi.encodeWithSignature( - "mintPending((bytes32,bytes32,bytes32,bytes32,uint128,uint80,uint48),uint256,uint256)", - teleportGUID, - maxFeePercentage, - operatorFee - )); - } - function testConstructor() public { assertEq(address(join.vat()), address(vat)); assertEq(address(join.daiJoin()), address(daiJoin)); @@ -130,44 +82,79 @@ contract TeleportJoinTest is DSTest { function testRelyDeny() public { assertEq(join.wards(address(456)), 0); - assertTrue(_tryRely(address(456))); + join.rely(address(456)); assertEq(join.wards(address(456)), 1); - assertTrue(_tryDeny(address(456))); + join.deny(address(456)); assertEq(join.wards(address(456)), 0); join.deny(address(this)); - assertTrue(!_tryRely(address(456))); - assertTrue(!_tryDeny(address(456))); + vm.expectRevert("TeleportJoin/not-authorized"); + join.rely(address(456)); + vm.expectRevert("TeleportJoin/not-authorized"); + join.deny(address(456)); } function testFile() public { assertEq(join.vow(), vow); - assertTrue(_tryFile("vow", address(888))); + join.file("vow", address(888)); assertEq(join.vow(), address(888)); assertEq(join.fees("aaa"), address(0)); - assertTrue(_tryFile("fees", "aaa", address(888))); + join.file("fees", "aaa", address(888)); assertEq(join.fees("aaa"), address(888)); assertEq(join.line("aaa"), 0); uint256 maxInt256 = uint256(type(int256).max); - assertTrue(_tryFile("line", "aaa", maxInt256)); + join.file("line", "aaa", maxInt256); assertEq(join.line("aaa"), maxInt256); - assertTrue(!_tryFile("line", "aaa", maxInt256 + 1)); + vm.expectRevert("TeleportJoin/not-allowed-bigger-int256"); + join.file("line", "aaa", maxInt256 + 1); join.deny(address(this)); - assertTrue(!_tryFile("vow", address(888))); - assertTrue(!_tryFile("fees", "aaa", address(888))); - assertTrue(!_tryFile("line", "aaa", 10)); + vm.expectRevert("TeleportJoin/not-authorized"); + join.file("vow", address(888)); + vm.expectRevert("TeleportJoin/not-authorized"); + join.file("fees", "aaa", address(888)); + vm.expectRevert("TeleportJoin/not-authorized"); + join.file("line", "aaa", 10); } function testInvalidWhat() public { - assertTrue(!_tryFile("meh", address(888))); - assertTrue(!_tryFile("meh", domain, address(888))); - assertTrue(!_tryFile("meh", domain, 888)); + vm.expectRevert("TeleportJoin/file-unrecognized-param"); + join.file("meh", address(888)); + vm.expectRevert("TeleportJoin/file-unrecognized-param"); + join.file("meh", domain, address(888)); + vm.expectRevert("TeleportJoin/file-unrecognized-param"); + join.file("meh", domain, 888); + } + + function testRegister() public { + TeleportGUID memory guid = TeleportGUID({ + sourceDomain: "l2network", + targetDomain: "ethereum", + receiver: addressToBytes32(address(123)), + operator: addressToBytes32(address(this)), + amount: 250_000 ether, + nonce: 5, + timestamp: uint48(block.timestamp) + }); + + assertEq(dai.balanceOf(address(123)), 0); + assertTrue(!_blessed(guid)); + assertEq(_pending(guid), 0); + assertEq(_ink(), 0); + assertEq(_art(), 0); + + join.registerMint(guid); + + assertEq(dai.balanceOf(address(123)), 0); + assertTrue(_blessed(guid)); + assertEq(_pending(guid), 250_000 ether); + assertEq(_ink(), 0); + assertEq(_art(), 0); } function testRegisterAndWithdrawAll() public { @@ -258,8 +245,9 @@ contract TeleportJoinTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); - assertTrue(!_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); + vm.expectRevert("TeleportJoin/already-blessed"); + join.requestMint(guid, 0, 0); } function testRegisterWrongDomain() public { @@ -272,7 +260,8 @@ contract TeleportJoinTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - assertTrue(!_tryRequestMint(guid, 0, 0)); + vm.expectRevert("TeleportJoin/incorrect-domain"); + join.requestMint(guid, 0, 0); } function testRegisterAndWithdrawPayingFee() public { @@ -314,7 +303,8 @@ contract TeleportJoinTest is DSTest { }); join.file("fees", "l2network", address(new TeleportConstantFee(100 ether, TTL))); - assertTrue(!_tryRequestMint(guid, 3 * WAD / 10000, 0)); // 0.03% * 250K < 100 (not enough) + vm.expectRevert("TeleportJoin/max-fee-exceed"); + join.requestMint(guid, 3 * WAD / 10000, 0); // 0.03% * 250K < 100 (not enough) } function testRegisterAndWithdrawFeeTTLExpires() public { @@ -332,8 +322,8 @@ contract TeleportJoinTest is DSTest { assertEq(fees.fee(), 100 ether); join.file("fees", "l2network", address(fees)); - hevm.warp(block.timestamp + TTL + 1 days); // Over ttl - you don't pay fees - assertTrue(_tryRequestMint(guid, 0, 0)); + vm.warp(block.timestamp + TTL + 1 days); // Over ttl - you don't pay fees + join.requestMint(guid, 0, 0); assertEq(vat.dai(vow), 0); assertEq(dai.balanceOf(address(123)), 250_000 ether); @@ -358,7 +348,7 @@ contract TeleportJoinTest is DSTest { join.file("line", "l2network", 200_000 ether); join.file("fees", "l2network", address(new TeleportConstantFee(100 ether, TTL))); - assertTrue(_tryRequestMint(guid, 4 * WAD / 10000, 0)); // 0.04% * 200K = 80 (just enough as fee is also proportional) + join.requestMint(guid, 4 * WAD / 10000, 0); // 0.04% * 200K = 80 (just enough as fee is also proportional) assertEq(vat.dai(vow), 80 * RAD); assertEq(dai.balanceOf(address(123)), 199_920 ether); @@ -370,7 +360,7 @@ contract TeleportJoinTest is DSTest { join.file("line", "l2network", 250_000 ether); - assertTrue(_tryMintPending(guid, 4 * WAD / 10000, 0)); // 0.04% * 50 = 20 (just enough as fee is also proportional) + join.mintPending(guid, 4 * WAD / 10000, 0); // 0.04% * 50 = 20 (just enough as fee is also proportional) assertEq(vat.dai(vow), 100 * RAD); assertEq(dai.balanceOf(address(123)), 249_900 ether); @@ -395,7 +385,8 @@ contract TeleportJoinTest is DSTest { join.file("line", "l2network", 200_000 ether); join.file("fees", "l2network", address(new TeleportConstantFee(100 ether, TTL))); - assertTrue(!_tryRequestMint(guid, 3 * WAD / 10000, 0)); // 0.03% * 200K < 80 (not enough) + vm.expectRevert("TeleportJoin/max-fee-exceed"); + join.requestMint(guid, 3 * WAD / 10000, 0); // 0.03% * 200K < 80 (not enough) } function testRegisterAndWithdrawPartialPayingFee2() public { @@ -413,11 +404,12 @@ contract TeleportJoinTest is DSTest { join.file("line", "l2network", 200_000 ether); join.file("fees", "l2network", address(new TeleportConstantFee(100 ether, TTL))); - assertTrue(_tryRequestMint(guid, 4 * WAD / 10000, 0)); + join.requestMint(guid, 4 * WAD / 10000, 0); join.file("line", "l2network", 250_000 ether); - assertTrue(!_tryMintPending(guid, 3 * WAD / 10000, 0)); // 0.03% * 50 < 20 (not enough) + vm.expectRevert("TeleportJoin/max-fee-exceed"); + join.mintPending(guid, 3 * WAD / 10000, 0); // 0.03% * 50 < 20 (not enough) } function testMintPendingByOperator() public { @@ -432,14 +424,14 @@ contract TeleportJoinTest is DSTest { }); join.file("line", "l2network", 200_000 ether); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(dai.balanceOf(address(this)), 200_000 ether); assertTrue(_blessed(guid)); assertEq(_pending(guid), 50_000 ether); join.file("line", "l2network", 225_000 ether); - assertTrue(_tryMintPending(guid, 0, 0)); + join.mintPending(guid, 0, 0); assertEq(dai.balanceOf(address(this)), 225_000 ether); assertEq(_pending(guid), 25_000 ether); @@ -457,14 +449,14 @@ contract TeleportJoinTest is DSTest { }); join.file("line", "l2network", 200_000 ether); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(dai.balanceOf(address(123)), 200_000 ether); assertTrue(_blessed(guid)); assertEq(_pending(guid), 50_000 ether); join.file("line", "l2network", 225_000 ether); - assertTrue(_tryMintPending(guid, 0, 0)); + join.mintPending(guid, 0, 0); assertEq(dai.balanceOf(address(123)), 225_000 ether); assertEq(_pending(guid), 25_000 ether); @@ -482,14 +474,14 @@ contract TeleportJoinTest is DSTest { }); join.file("line", "l2network", 200_000 ether); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(dai.balanceOf(address(this)), 200_000 ether); assertTrue(_blessed(guid)); assertEq(_pending(guid), 50_000 ether); join.file("line", "l2network", 225_000 ether); - assertTrue(_tryMintPending(guid, 0, 0)); + join.mintPending(guid, 0, 0); assertEq(dai.balanceOf(address(this)), 225_000 ether); assertEq(_pending(guid), 25_000 ether); @@ -507,10 +499,11 @@ contract TeleportJoinTest is DSTest { }); join.file("line", "l2network", 200_000 ether); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); join.file("line", "l2network", 225_000 ether); - assertTrue(!_tryMintPending(guid, 0, 0)); + vm.expectRevert("TeleportJoin/not-receiver-nor-operator"); + join.mintPending(guid, 0, 0); } function testSettle() public { @@ -519,7 +512,7 @@ contract TeleportJoinTest is DSTest { vat.suck(address(0), address(this), 100_000 * RAD); daiJoin.exit(address(join), 100_000 ether); - join.settle("l2network", 100_000 ether); + join.settle("l2network", domain, 100_000 ether); assertEq(join.debt("l2network"), -100_000 ether); assertEq(join.cure(), 0); @@ -529,7 +522,7 @@ contract TeleportJoinTest is DSTest { vat.suck(address(0), address(this), 100_000 * RAD); daiJoin.exit(address(join), 100_000 ether); - join.settle("l2network", 100_000 ether); + join.settle("l2network", domain, 100_000 ether); assertEq(join.debt("l2network"), -100_000 ether); assertEq(join.cure(), 0); @@ -544,7 +537,7 @@ contract TeleportJoinTest is DSTest { timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(dai.balanceOf(address(123)), 250_000 ether); assertEq(_ink(), 150_000 ether); @@ -556,7 +549,7 @@ contract TeleportJoinTest is DSTest { vat.suck(address(0), address(this), 100_000 * RAD); daiJoin.exit(address(join), 100_000 ether); - join.settle("l2network", 100_000 ether); + join.settle("l2network", domain, 100_000 ether); assertEq(join.debt("l2network"), -100_000 ether); assertEq(join.cure(), 0); @@ -572,7 +565,7 @@ contract TeleportJoinTest is DSTest { }); join.file("line", "l2network", 100_000 ether); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(dai.balanceOf(address(123)), 200_000 ether); assertEq(_pending(guid), 50_000 ether); @@ -585,7 +578,7 @@ contract TeleportJoinTest is DSTest { vat.suck(address(0), address(this), 100_000 * RAD); daiJoin.exit(address(join), 100_000 ether); - join.settle("l2network", 100_000 ether); + join.settle("l2network", domain, 100_000 ether); assertEq(join.debt("l2network"), -100_000 ether); assertEq(join.cure(), 0); @@ -604,7 +597,7 @@ contract TeleportJoinTest is DSTest { assertEq(vat.live(), 0); join.file("fees", "l2network", address(new TeleportConstantFee(100 ether, TTL))); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(dai.balanceOf(address(123)), 100_000 ether); // Can't pay more than DAI is already in the join assertEq(_pending(guid), 150_000 ether); @@ -625,7 +618,7 @@ contract TeleportJoinTest is DSTest { timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(join.debt("l2network"), 250_000 ether); assertEq(_ink(), 250_000 ether); @@ -637,7 +630,7 @@ contract TeleportJoinTest is DSTest { vat.suck(address(0), address(this), 250_000 * RAD); daiJoin.exit(address(join), 250_000 ether); - join.settle("l2network", 250_000 ether); + join.settle("l2network", domain, 250_000 ether); assertEq(join.debt("l2network"), 0); assertEq(_ink(), 250_000 ether); @@ -677,7 +670,8 @@ contract TeleportJoinTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - assertTrue(!_tryRequestMint(guid, 0, 250_001 ether)); // Slightly over the amount + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); + join.requestMint(guid, 0, 250_001 ether); // Slightly over the amount } function testRegisterAndWithdrawPartialPayingOperatorFee() public { @@ -692,7 +686,7 @@ contract TeleportJoinTest is DSTest { }); join.file("line", "l2network", 200_000 ether); - assertTrue(_tryRequestMint(guid, 0, 200 ether)); + join.requestMint(guid, 0, 200 ether); assertEq(dai.balanceOf(address(this)), 200 ether); assertEq(dai.balanceOf(address(123)), 199_800 ether); @@ -702,7 +696,7 @@ contract TeleportJoinTest is DSTest { assertEq(_art(), 200_000 ether); join.file("line", "l2network", 250_000 ether); - assertTrue(_tryMintPending(guid, 0, 5 ether)); + join.mintPending(guid, 0, 5 ether); assertEq(dai.balanceOf(address(this)), 205 ether); assertEq(dai.balanceOf(address(123)), 249_795 ether); @@ -723,7 +717,7 @@ contract TeleportJoinTest is DSTest { }); assertEq(dai.balanceOf(address(this)), 0); join.file("fees", "l2network", address(new TeleportConstantFee(1000 ether, TTL))); - assertTrue(_tryRequestMint(guid, 40 ether / 10000, 249 ether)); + join.requestMint(guid, 40 ether / 10000, 249 ether); assertEq(dai.balanceOf(address(this)), 249 ether); assertEq(vat.dai(vow), 1000 * RAD); assertEq(dai.balanceOf(address(123)), 248_751 ether); @@ -743,7 +737,8 @@ contract TeleportJoinTest is DSTest { timestamp: uint48(block.timestamp) }); join.file("fees", "l2network", address(new TeleportConstantFee(1000 ether, TTL))); - assertTrue(!_tryRequestMint(guid, 40 ether / 10000, 249_001 ether)); // Too many fees + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x11)); + join.requestMint(guid, 40 ether / 10000, 249_001 ether); // Too many fees } function testTotalDebtSeveralDomains() public { @@ -754,7 +749,8 @@ contract TeleportJoinTest is DSTest { vat.suck(address(0), address(this), 100_000 * RAD); daiJoin.exit(address(join), 100_000 ether); - join.settle("l2network", 100_000 ether); + + join.settle("l2network", domain, 100_000 ether); TeleportGUID memory guid = TeleportGUID({ sourceDomain: "l2network_2", @@ -765,7 +761,7 @@ contract TeleportJoinTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); guid = TeleportGUID({ sourceDomain: "l2network_3", @@ -776,7 +772,7 @@ contract TeleportJoinTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(join.debt("l2network"), -100_000 ether); assertEq(join.debt("l2network_2"), 150_000 ether); @@ -792,7 +788,7 @@ contract TeleportJoinTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(join.debt("l2network"), -50_000 ether); assertEq(join.debt("l2network_2"), 150_000 ether); @@ -801,7 +797,8 @@ contract TeleportJoinTest is DSTest { vat.suck(address(0), address(this), 10_000 * RAD); daiJoin.exit(address(join), 10_000 ether); - join.settle("l2network_3", 10_000 ether); + + join.settle("l2network_3", domain, 10_000 ether); assertEq(join.debt("l2network"), -50_000 ether); assertEq(join.debt("l2network_2"), 150_000 ether); @@ -820,14 +817,14 @@ contract TeleportJoinTest is DSTest { timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(_ink(), 250_000 ether); assertEq(_art(), 250_000 ether); assertEq(join.cure(), 250_000 * RAD); // Emulate removal of position debt (third party repayment or position being skimmed) - hevm.store( + vm.store( address(vat), bytes32(uint256(keccak256(abi.encode(address(join), keccak256(abi.encode(bytes32(ilk), uint256(2)))))) + 1), bytes32(0) @@ -846,7 +843,7 @@ contract TeleportJoinTest is DSTest { timestamp: uint48(block.timestamp) }); - assertTrue(_tryRequestMint(guid, 0, 0)); + join.requestMint(guid, 0, 0); assertEq(_ink(), 350_000 ether); assertEq(_art(), 100_000 ether); diff --git a/src/test/TeleportLinearFee.t.sol b/src/test/TeleportLinearFee.t.sol index 692e15b..28f0e14 100644 --- a/src/test/TeleportLinearFee.t.sol +++ b/src/test/TeleportLinearFee.t.sol @@ -16,7 +16,7 @@ pragma solidity 0.8.15; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import "src/TeleportLinearFee.sol"; import "src/TeleportGUID.sol"; @@ -25,9 +25,7 @@ interface Hevm { function warp(uint) external; } -contract TeleportLinearFeeTest is DSTest { - - Hevm internal hevm = Hevm(HEVM_ADDRESS); +contract TeleportLinearFeeTest is Test { uint256 internal fee = 1 ether / 10000; // 1 BPS fee uint256 internal ttl = 8 days; @@ -66,7 +64,7 @@ contract TeleportLinearFeeTest is DSTest { nonce: 5, timestamp: uint48(block.timestamp) }); - hevm.warp(block.timestamp + ttl); + vm.warp(block.timestamp + ttl); assertEq(teleportLinearFee.getFee(guid, 0, 0, 0, 100 ether), 0); } diff --git a/src/test/TeleportOracleAuth.t.sol b/src/test/TeleportOracleAuth.t.sol index 6a8d0a6..cacac33 100644 --- a/src/test/TeleportOracleAuth.t.sol +++ b/src/test/TeleportOracleAuth.t.sol @@ -16,7 +16,7 @@ pragma solidity 0.8.15; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import "src/TeleportOracleAuth.sol"; @@ -27,9 +27,7 @@ interface Hevm { function sign(uint, bytes32) external returns (uint8, bytes32, bytes32); } -contract TeleportOracleAuthTest is DSTest { - - Hevm hevm = Hevm(HEVM_ADDRESS); +contract TeleportOracleAuthTest is Test { TeleportOracleAuth internal auth; address internal teleportJoin; @@ -38,39 +36,6 @@ contract TeleportOracleAuthTest is DSTest { auth = new TeleportOracleAuth(teleportJoin); } - function _tryRely(address usr) internal returns (bool ok) { - (ok,) = address(auth).call(abi.encodeWithSignature("rely(address)", usr)); - } - - function _tryDeny(address usr) internal returns (bool ok) { - (ok,) = address(auth).call(abi.encodeWithSignature("deny(address)", usr)); - } - - function _tryFile(bytes32 what, uint256 data) internal returns (bool ok) { - (ok,) = address(auth).call(abi.encodeWithSignature("file(bytes32,uint256)", what, data)); - } - - function _tryIsValid(bytes32 signHash, bytes memory signatures, uint256 threshold_) internal returns (bool ok) { - bytes memory res; - (ok, res) = address(auth).call(abi.encodeWithSignature("isValid(bytes32,bytes,uint256)", signHash, signatures, threshold_)); - return ok && abi.decode(res, (bool)); - } - - function _tryRequestMint( - TeleportGUID memory teleportGUID, - bytes memory signatures, - uint256 maxFeePercentage, - uint256 operatorFee - ) internal returns (bool ok) { - (ok,) = address(auth).call(abi.encodeWithSignature( - "requestMint((bytes32,bytes32,bytes32,bytes32,uint128,uint80,uint48),bytes,uint256,uint256)", - teleportGUID, - signatures, - maxFeePercentage, - operatorFee - )); - } - function getSignatures(bytes32 signHash) internal returns (bytes memory signatures, address[] memory signers) { // seeds chosen s.t. corresponding addresses are in ascending order uint8[30] memory seeds = [8,10,6,2,9,15,14,20,7,29,24,13,12,25,16,26,21,22,0,18,17,27,3,28,23,19,4,5,1,11]; @@ -78,8 +43,8 @@ contract TeleportOracleAuthTest is DSTest { signers = new address[](numSigners); for(uint i; i < numSigners; i++) { uint sk = uint256(keccak256(abi.encode(seeds[i]))); - signers[i] = hevm.addr(sk); - (uint8 v, bytes32 r, bytes32 s) = hevm.sign(sk, signHash); + signers[i] = vm.addr(sk); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(sk, signHash); signatures = abi.encodePacked(signatures, r, s, v); } assertEq(signatures.length, numSigners * 65); @@ -92,33 +57,37 @@ contract TeleportOracleAuthTest is DSTest { function testRelyDeny() public { assertEq(auth.wards(address(456)), 0); - assertTrue(_tryRely(address(456))); + auth.rely(address(456)); assertEq(auth.wards(address(456)), 1); - assertTrue(_tryDeny(address(456))); + auth.deny(address(456)); assertEq(auth.wards(address(456)), 0); auth.deny(address(this)); - assertTrue(!_tryRely(address(456))); - assertTrue(!_tryDeny(address(456))); + vm.expectRevert("TeleportOracleAuth/not-authorized"); + auth.rely(address(456)); + vm.expectRevert("TeleportOracleAuth/not-authorized"); + auth.deny(address(456)); } function testFileFailsWhenNotAuthed() public { - assertTrue(_tryFile("threshold", 888)); + auth.file("threshold", 888); auth.deny(address(this)); - assertTrue(!_tryFile("threshold", 888)); + vm.expectRevert("TeleportOracleAuth/not-authorized"); + auth.file("threshold", 888); } function testFileNewThreshold() public { assertEq(auth.threshold(), 0); - assertTrue(_tryFile("threshold", 3)); + auth.file("threshold", 3); assertEq(auth.threshold(), 3); } function testFileInvalidWhat() public { - assertTrue(!_tryFile("meh", 888)); + vm.expectRevert("TeleportOracleAuth/file-unrecognized-param"); + auth.file("meh", 888); } function testAddRemoveSigners() public { @@ -145,7 +114,7 @@ contract TeleportOracleAuthTest is DSTest { bytes32 signHash = keccak256("msg"); (bytes memory signatures, address[] memory signers) = getSignatures(signHash); auth.addSigners(signers); - assertTrue(_tryIsValid(signHash, signatures, signers.length)); + assertTrue(auth.isValid(signHash, signatures, signers.length)); } // Since ecrecover silently returns 0 on failure, it's a good idea to make sure @@ -163,14 +132,16 @@ contract TeleportOracleAuthTest is DSTest { signers[0] = address(0); auth.addSigners(signers); - assertTrue(!_tryIsValid(signHash, signatures, signers.length)); + vm.expectRevert("TeleportOracleAuth/bad-sig-order"); + auth.isValid(signHash, signatures, signers.length); } function test_isValid_notEnoughSig() public { bytes32 signHash = keccak256("msg"); (bytes memory signatures, address[] memory signers) = getSignatures(signHash); auth.addSigners(signers); - assertTrue(!_tryIsValid(signHash, signatures, signers.length + 1)); + vm.expectRevert("TeleportOracleAuth/not-enough-sig"); + auth.isValid(signHash, signatures, signers.length + 1); } function test_isValid_badSig() public { @@ -178,7 +149,8 @@ contract TeleportOracleAuthTest is DSTest { (bytes memory signatures, address[] memory signers) = getSignatures(signHash); auth.addSigners(signers); signatures[0] = bytes1(uint8((uint256(uint8(signatures[0])) + 1) % 256)); - assertTrue(!_tryIsValid(signHash, signatures, signers.length)); + vm.expectRevert("TeleportOracleAuth/bad-sig-order"); + auth.isValid(signHash, signatures, signers.length); } function test_mintByOperator() public { @@ -195,7 +167,7 @@ contract TeleportOracleAuthTest is DSTest { uint maxFee = 0; - assertTrue(_tryRequestMint(guid, signatures, maxFee, 0)); + auth.requestMint(guid, signatures, maxFee, 0); } function test_mintByOperatorNotReceiver() public { @@ -212,7 +184,7 @@ contract TeleportOracleAuthTest is DSTest { uint maxFee = 0; - assertTrue(_tryRequestMint(guid, signatures, maxFee, 0)); + auth.requestMint(guid, signatures, maxFee, 0); } function test_mintByReceiver() public { @@ -229,7 +201,7 @@ contract TeleportOracleAuthTest is DSTest { uint maxFee = 0; - assertTrue(_tryRequestMint(guid, signatures, maxFee, 0)); + auth.requestMint(guid, signatures, maxFee, 0); } function test_mint_notOperatorNorReceiver() public { @@ -246,7 +218,8 @@ contract TeleportOracleAuthTest is DSTest { uint maxFee = 0; - assertTrue(!_tryRequestMint(guid, signatures, maxFee, 0)); + vm.expectRevert("TeleportOracleAuth/not-receiver-nor-operator"); + auth.requestMint(guid, signatures, maxFee, 0); } } diff --git a/src/test/TeleportRouter.t.sol b/src/test/TeleportRouter.t.sol index 7daea34..c437c5d 100644 --- a/src/test/TeleportRouter.t.sol +++ b/src/test/TeleportRouter.t.sol @@ -16,142 +16,105 @@ pragma solidity 0.8.15; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import "src/TeleportRouter.sol"; import "./mocks/GatewayMock.sol"; import "./mocks/DaiMock.sol"; -contract TeleportRouterTest is DSTest { +contract TeleportRouterTest is Test { TeleportRouter internal router; address internal dai; address internal teleportJoin; - bytes32 constant internal l1Domain = "ethereum"; + bytes32 constant internal domain = "rollup"; + bytes32 constant internal parentDomain = "ethereum"; uint256 internal constant WAD = 10**18; function setUp() public { dai = address(new DaiMock()); teleportJoin = address(new GatewayMock()); - router = new TeleportRouter(dai); - } - - function _tryRely(address usr) internal returns (bool ok) { - (ok,) = address(router).call(abi.encodeWithSignature("rely(address)", usr)); - } - - function _tryDeny(address usr) internal returns (bool ok) { - (ok,) = address(router).call(abi.encodeWithSignature("deny(address)", usr)); - } - - function _tryFile(bytes32 what, bytes32 domain, address data) internal returns (bool ok) { - (ok,) = address(router).call(abi.encodeWithSignature("file(bytes32,bytes32,address)", what, domain, data)); - } - - function _tryRequestMint( - TeleportGUID memory teleportGUID, - uint256 maxFeePercentage, - uint256 operatorFee - ) internal returns (bool ok) { - (ok,) = address(router).call(abi.encodeWithSignature( - "requestMint((bytes32,bytes32,bytes32,bytes32,uint128,uint80,uint48),uint256,uint256)", - teleportGUID, - maxFeePercentage, - operatorFee - )); - } - function _trySettle(bytes32 targetDomain, uint256 batchedDaiToFlush) internal returns (bool ok) { - (ok,) = address(router).call(abi.encodeWithSignature("settle(bytes32,uint256)", targetDomain, batchedDaiToFlush)); + router = new TeleportRouter(dai, domain, parentDomain); } function testConstructor() public { assertEq(address(router.dai()), dai); + assertEq(router.domain(), domain); + assertEq(router.parentDomain(), parentDomain); assertEq(router.wards(address(this)), 1); } function testRelyDeny() public { assertEq(router.wards(address(456)), 0); - assertTrue(_tryRely(address(456))); + router.rely(address(456)); assertEq(router.wards(address(456)), 1); - assertTrue(_tryDeny(address(456))); + router.deny(address(456)); assertEq(router.wards(address(456)), 0); router.deny(address(this)); - assertTrue(!_tryRely(address(456))); - assertTrue(!_tryDeny(address(456))); - } - - function testFileFailsWhenNotAuthed() public { - assertTrue(_tryFile("gateway", "dom", address(888))); - router.deny(address(this)); - assertTrue(!_tryFile("gateway", "dom", address(888))); + vm.expectRevert("TeleportRouter/not-authorized"); + router.rely(address(456)); + vm.expectRevert("TeleportRouter/not-authorized"); + router.deny(address(456)); } function testFileNewDomains() public { bytes32 domain1 = "newdom1"; address gateway1 = address(111); assertEq(router.gateways(domain1), address(0)); - assertEq(router.domains(gateway1), bytes32(0)); assertEq(router.numDomains(), 0); - assertTrue(_tryFile("gateway", domain1, gateway1)); + router.file("gateway", domain1, gateway1); assertEq(router.gateways(domain1), gateway1); - assertEq(router.domains(gateway1), domain1); assertEq(router.numDomains(), 1); assertEq(router.domainAt(0), domain1); bytes32 domain2 = "newdom2"; address gateway2 = address(222); assertEq(router.gateways(domain2), address(0)); - assertEq(router.domains(gateway2), bytes32(0)); - assertTrue(_tryFile("gateway", domain2, gateway2)); + router.file("gateway", domain2, gateway2); assertEq(router.gateways(domain2), gateway2); - assertEq(router.domains(gateway2), domain2); assertEq(router.numDomains(), 2); assertEq(router.domainAt(0), domain1); assertEq(router.domainAt(1), domain2); } function testFileNewGatewayForExistingDomain() public { - bytes32 domain = "dom"; + bytes32 domain1 = "dom"; address gateway1 = address(111); - assertTrue(_tryFile("gateway", domain, gateway1)); - assertEq(router.gateways(domain), gateway1); - assertEq(router.domains(gateway1), domain); + router.file("gateway", domain1, gateway1); + assertEq(router.gateways(domain1), gateway1); assertEq(router.numDomains(), 1); - assertEq(router.domainAt(0), domain); + assertEq(router.domainAt(0), domain1); address gateway2 = address(222); - assertTrue(_tryFile("gateway", domain, gateway2)); + router.file("gateway", domain1, gateway2); - assertEq(router.gateways(domain), gateway2); - assertEq(router.domains(gateway1), bytes32(0)); - assertEq(router.domains(gateway2), domain); + assertEq(router.gateways(domain1), gateway2); assertEq(router.numDomains(), 1); - assertEq(router.domainAt(0), domain); + assertEq(router.domainAt(0), domain1); } function testFileRemoveLastDomain() public { - bytes32 domain = "dom"; + bytes32 domain1 = "dom"; address gateway = address(111); - assertTrue(_tryFile("gateway", domain, gateway)); - assertEq(router.gateways(domain), gateway); - assertEq(router.domains(gateway), domain); + router.file("gateway", domain1, gateway); + assertEq(router.gateways(domain1), gateway); assertEq(router.numDomains(), 1); - assertEq(router.domainAt(0), domain); + assertEq(router.domainAt(0), domain1); - // Remove last domain - assertTrue(_tryFile("gateway", domain, address(0))); + // Remove last domain1 + router.file("gateway", domain1, address(0)); - assertEq(router.gateways(domain), address(0)); - assertEq(router.domains(gateway), bytes32(0)); - assertTrue(!router.hasDomain(domain)); + assertEq(router.gateways(domain1), address(0)); + assertTrue(!router.hasDomain(domain1)); + assertEq(router.numDomains(), 0); } @@ -160,46 +123,132 @@ contract TeleportRouterTest is DSTest { bytes32 domain2 = "dom2"; address gateway1 = address(111); address gateway2 = address(222); - assertTrue(_tryFile("gateway", domain1, gateway1)); - assertTrue(_tryFile("gateway", domain2, gateway2)); + router.file("gateway", domain1, gateway1); + router.file("gateway", domain2, gateway2); assertEq(router.gateways(domain1), gateway1); assertEq(router.gateways(domain2), gateway2); - assertEq(router.domains(gateway1), domain1); - assertEq(router.domains(gateway2), domain2); assertEq(router.numDomains(), 2); assertEq(router.domainAt(0), domain1); assertEq(router.domainAt(1), domain2); // Remove first domain - assertTrue(_tryFile("gateway", domain1, address(0))); + router.file("gateway", domain1, address(0)); assertEq(router.gateways(domain1), address(0)); assertEq(router.gateways(domain2), gateway2); - assertEq(router.domains(gateway1), bytes32(0)); - assertEq(router.domains(gateway2), domain2); assertEq(router.numDomains(), 1); assertEq(router.domainAt(0), domain2); // Re-add removed domain - assertTrue(_tryFile("gateway", domain1, gateway1)); + router.file("gateway", domain1, gateway1); assertEq(router.gateways(domain1), gateway1); assertEq(router.gateways(domain2), gateway2); - assertEq(router.domains(gateway1), domain1); - assertEq(router.domains(gateway2), domain2); assertEq(router.numDomains(), 2); assertEq(router.domainAt(0), domain2); // domains have been swapped compared to initial state assertEq(router.domainAt(1), domain1); } + function testFileTwoDomainsSameGateway() public { + bytes32 domain1 = "dom1"; + bytes32 domain2 = "dom2"; + address gateway1 = address(111); + router.file("gateway", domain1, gateway1); + router.file("gateway", domain2, gateway1); + assertEq(router.gateways(domain1), gateway1); + assertEq(router.gateways(domain2), gateway1); + assertEq(router.numDomains(), 2); + assertEq(router.domainAt(0), domain1); + assertEq(router.domainAt(1), domain2); + } + + function testFileTwoDomainsSameGatewayRemove1() public { + bytes32 domain1 = "dom1"; + bytes32 domain2 = "dom2"; + address gateway1 = address(111); + router.file("gateway", domain1, gateway1); + router.file("gateway", domain2, gateway1); + + router.file("gateway", domain2, address(0)); + + assertEq(router.gateways(domain1), gateway1); + assertEq(router.gateways(domain2), address(0)); + assertEq(router.numDomains(), 1); + assertEq(router.domainAt(0), domain1); + } + + function testFileTwoDomainsSameGatewayRemove2() public { + bytes32 domain1 = "dom1"; + bytes32 domain2 = "dom2"; + address gateway1 = address(111); + router.file("gateway", domain1, gateway1); + router.file("gateway", domain2, gateway1); + + router.file("gateway", domain1, address(0)); + router.file("gateway", domain2, address(0)); + + assertEq(router.gateways(domain1), address(0)); + assertEq(router.gateways(domain2), address(0)); + assertEq(router.numDomains(), 0); + } + + function testFileTwoDomainsSameGatewaySplit() public { + bytes32 domain1 = "dom1"; + bytes32 domain2 = "dom2"; + address gateway1 = address(111); + address gateway2 = address(222); + router.file("gateway", domain1, gateway1); + router.file("gateway", domain2, gateway1); + + router.file("gateway", domain2, gateway2); + + assertEq(router.gateways(domain1), gateway1); + assertEq(router.gateways(domain2), gateway2); + assertEq(router.numDomains(), 2); + assertEq(router.domainAt(0), domain1); + assertEq(router.domainAt(1), domain2); + } + + function testFileTwoDomainsSameGatewaySplitRemove() public { + bytes32 domain1 = "dom1"; + bytes32 domain2 = "dom2"; + address gateway1 = address(111); + address gateway2 = address(222); + router.file("gateway", domain1, gateway1); + router.file("gateway", domain2, gateway1); + + router.file("gateway", domain2, gateway2); + router.file("gateway", domain1, address(0)); + + assertEq(router.gateways(domain1), address(0)); + assertEq(router.gateways(domain2), gateway2); + assertEq(router.numDomains(), 1); + assertEq(router.domainAt(0), domain2); + } + + function testFile() public { + assertEq(router.fdust(), 0); + router.file("fdust", 888); + assertEq(router.fdust(), 888); + } + function testFileInvalidWhat() public { - assertTrue(!_tryFile("meh", "aaa", address(888))); + vm.expectRevert("TeleportRouter/file-unrecognized-param"); + router.file("meh", "aaa", address(888)); + } + + function testFileFailsWhenNotAuthed() public { + router.deny(address(this)); + vm.expectRevert("TeleportRouter/not-authorized"); + router.file("gateway", "dom", address(888)); + vm.expectRevert("TeleportRouter/not-authorized"); + router.file("fdust", 1); } - function testRequestMintFromNotGateway() public { + function testRegisterMintFromNotGateway() public { TeleportGUID memory guid = TeleportGUID({ sourceDomain: "l2network", - targetDomain: l1Domain, + targetDomain: domain, receiver: addressToBytes32(address(123)), operator: addressToBytes32(address(234)), amount: 250_000 ether, @@ -208,13 +257,14 @@ contract TeleportRouterTest is DSTest { }); router.file("gateway", "l2network", address(555)); - assertTrue(!_tryRequestMint(guid, 4 * WAD / 10000, 0)); + vm.expectRevert("TeleportRouter/sender-not-gateway"); + router.registerMint(guid); } - function testRequestMintTargetingL1() public { + function testRegisterMintTargetingActualDomain() public { TeleportGUID memory guid = TeleportGUID({ sourceDomain: "l2network", - targetDomain: l1Domain, + targetDomain: domain, receiver: addressToBytes32(address(123)), operator: addressToBytes32(address(234)), amount: 250_000 ether, @@ -222,12 +272,12 @@ contract TeleportRouterTest is DSTest { timestamp: uint48(block.timestamp) }); router.file("gateway", "l2network", address(this)); - router.file("gateway", l1Domain, teleportJoin); + router.file("gateway", domain, teleportJoin); - assertTrue(_tryRequestMint(guid, 4 * WAD / 10000, 0)); + router.registerMint(guid); } - function testRequestMintTargetingL2() public { + function testRegisterMintTargetingSubDomain() public { TeleportGUID memory guid = TeleportGUID({ sourceDomain: "l2network", targetDomain: "another-l2network", @@ -240,10 +290,10 @@ contract TeleportRouterTest is DSTest { router.file("gateway", "l2network", address(this)); router.file("gateway", "another-l2network", address(new GatewayMock())); - assertTrue(_tryRequestMint(guid, 4 * WAD / 10000, 0)); + router.registerMint(guid); } - function testRequestMintTargetingInvalidDomain() public { + function testRegisterMintTargetingInvalidDomain() public { TeleportGUID memory guid = TeleportGUID({ sourceDomain: "l2network", targetDomain: "invalid-network", @@ -255,7 +305,24 @@ contract TeleportRouterTest is DSTest { }); router.file("gateway", "l2network", address(this)); - assertTrue(!_tryRequestMint(guid, 4 * WAD / 10000, 0)); + vm.expectRevert("TeleportRouter/unsupported-target-domain"); + router.registerMint(guid); + } + + function testRegisterMintFromParentGateway() public { + TeleportGUID memory guid = TeleportGUID({ + sourceDomain: "l2network", + targetDomain: "another-l2network", + receiver: addressToBytes32(address(123)), + operator: addressToBytes32(address(234)), + amount: 250_000 ether, + nonce: 5, + timestamp: uint48(block.timestamp) + }); + router.file("gateway", parentDomain, address(this)); + router.file("gateway", "another-l2network", address(new GatewayMock())); + + router.registerMint(guid); } function testSettleFromNotGateway() public { @@ -263,30 +330,94 @@ contract TeleportRouterTest is DSTest { DaiMock(dai).mint(address(this), 100 ether); DaiMock(dai).approve(address(router), 100 ether); - assertTrue(!_trySettle(l1Domain, 100 ether)); + vm.expectRevert("TeleportRouter/sender-not-gateway"); + router.settle("l2network", domain, 100 ether); } - function testSettleTargetingL1() public { + function testSettleTargetingActualDomain() public { router.file("gateway", "l2network", address(this)); - router.file("gateway", l1Domain, teleportJoin); + router.file("gateway", domain, teleportJoin); DaiMock(dai).mint(address(this), 100 ether); DaiMock(dai).approve(address(router), 100 ether); - assertTrue(_trySettle(l1Domain, 100 ether)); + router.settle("l2network", domain, 100 ether); } - function testSettleTargetingL2() public { + function testSettleTargetingSubDomain() public { router.file("gateway", "l2network", address(this)); router.file("gateway", "another-l2network", address(new GatewayMock())); DaiMock(dai).mint(address(this), 100 ether); DaiMock(dai).approve(address(router), 100 ether); - assertTrue(_trySettle("another-l2network", 100 ether)); + router.settle("l2network", "another-l2network", 100 ether); + } + + function testSettleFromParentGateway() public { + router.file("gateway", parentDomain, address(this)); + router.file("gateway", "another-l2network", address(new GatewayMock())); + DaiMock(dai).mint(address(this), 100 ether); + DaiMock(dai).approve(address(router), 100 ether); + + router.settle("l2network", "another-l2network", 100 ether); } function testSettleTargetingInvalidDomain() public { router.file("gateway", "l2network", address(this)); - assertTrue(!_trySettle("invalid-network", 100 ether)); + vm.expectRevert("TeleportRouter/unsupported-target-domain"); + router.settle("l2network", "invalid-network", 100 ether); + } + + function testInitiateTeleport() public { + address parentGateway = address(new GatewayMock()); + router.file("gateway", parentDomain, parentGateway); + DaiMock(dai).mint(address(this), 100_000 ether); + DaiMock(dai).approve(address(router), 100_000 ether); + + assertEq(DaiMock(dai).balanceOf(address(this)), 100_000 ether); + assertEq(DaiMock(dai).balanceOf(address(router)), 0); + assertEq(router.batches(parentDomain), 0); + assertEq(router.nonce(), 0); + + router.initiateTeleport(parentDomain, address(123), 100_000 ether); + + assertEq(DaiMock(dai).balanceOf(address(this)), 0); + assertEq(DaiMock(dai).balanceOf(address(router)), 100_000 ether); + assertEq(router.batches(parentDomain), 100_000 ether); + assertEq(router.nonce(), 1); + } + + function testFlush() public { + address parentGateway = address(new GatewayMock()); + router.file("gateway", parentDomain, parentGateway); + DaiMock(dai).mint(address(this), 100_000 ether); + DaiMock(dai).approve(address(router), 100_000 ether); + router.initiateTeleport(parentDomain, address(123), 100_000 ether); + + assertEq(router.batches(parentDomain), 100_000 ether); + assertEq(DaiMock(dai).balanceOf(address(router)), 100_000 ether); + assertEq(DaiMock(dai).balanceOf(parentGateway), 0); + + router.flush(parentDomain); + + assertEq(router.batches(parentDomain), 0); + assertEq(DaiMock(dai).balanceOf(address(router)), 0); + assertEq(DaiMock(dai).balanceOf(parentGateway), 100_000 ether); + } + + function testFlushDust() public { + address parentGateway = address(new GatewayMock()); + router.file("gateway", parentDomain, parentGateway); + DaiMock(dai).mint(address(this), 100_000 ether); + DaiMock(dai).approve(address(router), 100_000 ether); + router.initiateTeleport(parentDomain, address(123), 100_000 ether); + + assertEq(router.batches(parentDomain), 100_000 ether); + assertEq(DaiMock(dai).balanceOf(address(router)), 100_000 ether); + assertEq(DaiMock(dai).balanceOf(parentGateway), 0); + + router.file("fdust", 200_000 ether); + vm.expectRevert("TeleportRouter/flush-dust"); + router.flush(parentDomain); } } diff --git a/src/test/integration/TeleportJoin.t.sol b/src/test/integration/TeleportJoin.t.sol index 956fd52..99c9fe3 100644 --- a/src/test/integration/TeleportJoin.t.sol +++ b/src/test/integration/TeleportJoin.t.sol @@ -16,7 +16,7 @@ pragma solidity 0.8.15; -import "ds-test/test.sol"; +import "forge-std/Test.sol"; import {TeleportJoin} from "src/TeleportJoin.sol"; import "src/TeleportGUID.sol"; @@ -39,7 +39,7 @@ interface VatLike { function wards(address) external view returns (uint256); function rely(address) external; function init(bytes32) external; - function file(bytes32 ilk, bytes32 what, uint data) external; + function file(bytes32, bytes32, uint256) external; function dai(address) external view returns (uint256); function debt() external view returns (uint256); } @@ -58,13 +58,11 @@ interface CureLike { } interface TokenLike { - function transfer(address _to, uint256 _value) external returns (bool success); + function transfer(address, uint256) external returns (bool); + function approve(address, uint256) external returns (bool); } -contract TeleportJoinIntegrationTest is DSTest { - - Hevm internal hevm = Hevm(HEVM_ADDRESS); - +contract TeleportJoinIntegrationTest is Test { bytes32 constant internal ILK = "TELEPORT-ETHEREUM-MASTER-1"; bytes32 constant internal MASTER_DOMAIN = "ETHEREUM-MASTER-1"; bytes32 constant internal SLAVE_DOMAIN = "L2NETWORK-SLAVE-1"; @@ -83,7 +81,7 @@ contract TeleportJoinIntegrationTest is DSTest { uint256 internal constant TTL = 8 days; function getAuthFor(address auth) internal { - hevm.store( + vm.store( auth, keccak256(abi.encode(address(this), 0)), bytes32(uint256(1)) @@ -142,7 +140,7 @@ contract TeleportJoinIntegrationTest is DSTest { assertEq(vat.dai(address(teleportJoin)), 0); dai.transfer(address(teleportJoin), teleportAmount); - teleportJoin.settle(SLAVE_DOMAIN, teleportAmount); + teleportJoin.settle(SLAVE_DOMAIN, MASTER_DOMAIN, teleportAmount); assertEq(vat.dai(address(teleportJoin)), teleportAmount * RAY); // the dai is now locked in teleportJoin assertEq(teleportJoin.cure(), teleportAmount * RAY); // the debt was not actually settled @@ -156,8 +154,8 @@ contract TeleportJoinIntegrationTest is DSTest { // thaw the end uint256 vatDebt = vat.debt(); - hevm.warp(block.timestamp + end.wait()); - hevm.store(address(vat), keccak256(abi.encode(vow, 5)), bytes32(0)); // emulate clearing of vow dai + vm.warp(block.timestamp + end.wait()); + vm.store(address(vat), keccak256(abi.encode(vow, 5)), bytes32(0)); // emulate clearing of vow dai assertEq(vat.dai(vow), 0); end.thaw(); diff --git a/src/test/mocks/GatewayMock.sol b/src/test/mocks/GatewayMock.sol index 5ce68d5..dc2f7da 100644 --- a/src/test/mocks/GatewayMock.sol +++ b/src/test/mocks/GatewayMock.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.15; import "src/TeleportGUID.sol"; contract GatewayMock { + function registerMint(TeleportGUID calldata) external {} function requestMint(TeleportGUID calldata, uint256, uint256) external pure returns (uint256 postFeeAmount, uint256 totalFee) {} - function settle(bytes32 sourceDomain, uint256 batchedDaiToFlush) external {} + function settle(bytes32, bytes32, uint256) external {} }