diff --git a/l1-contracts/gas_report.md b/l1-contracts/gas_report.md index d86cd782f35d..fcb7904448f4 100644 --- a/l1-contracts/gas_report.md +++ b/l1-contracts/gas_report.md @@ -4,52 +4,52 @@ | 589795 | 2886 | | | | | | Function Name | min | avg | median | max | # calls | | L2_TOKEN_ADDRESS | 194 | 194 | 194 | 194 | 256 | -| UNDERLYING | 270 | 270 | 270 | 270 | 3052 | -| canonicalRollup | 1016 | 3623 | 5516 | 5516 | 5547 | -| depositToAztecPublic | 42745 | 127319 | 127958 | 127958 | 258 | +| UNDERLYING | 270 | 270 | 270 | 270 | 3075 | +| canonicalRollup | 1016 | 3620 | 5516 | 5516 | 5538 | +| depositToAztecPublic | 42812 | 127386 | 128025 | 128025 | 258 | | distributeFees | 27333 | 56798 | 57006 | 57006 | 258 | -| initialize | 48963 | 48963 | 48963 | 48963 | 1566 | +| initialize | 49029 | 49029 | 49029 | 49029 | 1566 | | src/core/Rollup.sol:Rollup contract | | | | | | |-------------------------------------|-----------------|---------|---------|----------|---------| | Deployment Cost | Deployment Size | | | | | -| 7875866 | 37825 | | | | | +| 7867837 | 37972 | | | | | | Function Name | min | avg | median | max | # calls | -| archive | 605 | 605 | 605 | 605 | 2474 | -| cheat__InitialiseValidatorSet | 751841 | 7048889 | 751865 | 13518099 | 519 | -| claimProverRewards | 31816 | 53337 | 34261 | 93936 | 3 | -| claimSequencerRewards | 57196 | 57196 | 57196 | 57196 | 1 | -| deposit | 169711 | 327728 | 342585 | 342585 | 256 | +| archive | 605 | 605 | 605 | 605 | 2475 | +| cheat__InitialiseValidatorSet | 752109 | 7052330 | 752133 | 13524799 | 519 | +| claimProverRewards | 31794 | 53315 | 34239 | 93914 | 3 | +| claimSequencerRewards | 57174 | 57174 | 57174 | 57174 | 1 | +| deposit | 169778 | 328470 | 342652 | 342652 | 256 | | getAttesters | 1970 | 26343 | 26629 | 26629 | 259 | -| getBlock | 1230 | 1230 | 1230 | 1230 | 883 | +| getBlock | 1230 | 1230 | 1230 | 1230 | 874 | | getCollectiveProverRewardsForEpoch | 636 | 1636 | 1636 | 2636 | 4 | -| getCurrentEpoch | 908 | 908 | 908 | 908 | 1032 | -| getCurrentEpochCommittee | 42026 | 42026 | 42026 | 42026 | 1 | -| getCurrentProposer | 44246 | 117389 | 52217 | 263958 | 795 | -| getCurrentSlot | 713 | 1332 | 713 | 4713 | 142 | +| getCurrentEpoch | 1017 | 1017 | 1017 | 1017 | 1032 | +| getCurrentEpochCommittee | 42004 | 42004 | 42004 | 42004 | 1 | +| getCurrentProposer | 44246 | 117399 | 52217 | 263958 | 795 | +| getCurrentSlot | 823 | 1442 | 823 | 4823 | 142 | | getEpochCommittee | 2010 | 14068 | 14218 | 14218 | 520 | | getEpochDuration | 439 | 439 | 439 | 439 | 256 | -| getFeeAssetPerEth | 1445 | 1445 | 1445 | 1445 | 1 | +| getFeeAssetPerEth | 1423 | 1423 | 1423 | 1423 | 1 | | getHasSubmitted | 942 | 1192 | 942 | 2942 | 8 | | getInbox | 476 | 581 | 476 | 2476 | 4926 | | getInfo | 1527 | 1527 | 1527 | 1527 | 16 | | getManaBaseFeeAt | 7834 | 15768 | 16535 | 16545 | 2333 | -| getOutbox | 518 | 895 | 518 | 2518 | 5434 | +| getOutbox | 496 | 873 | 496 | 2496 | 5434 | | getPendingBlockNumber | 507 | 507 | 507 | 507 | 1546 | | getProofSubmissionWindow | 404 | 404 | 404 | 404 | 4 | -| getProvenBlockNumber | 512 | 784 | 512 | 2512 | 7551 | +| getProvenBlockNumber | 490 | 762 | 490 | 2490 | 7553 | | getProvingCostPerManaInEth | 429 | 429 | 429 | 429 | 1 | | getProvingCostPerManaInFeeAsset | 4147 | 4147 | 4147 | 4147 | 1 | | getSequencerRewards | 671 | 1071 | 671 | 2671 | 5 | -| getSlasher | 518 | 518 | 518 | 518 | 518 | -| getSlotDuration | 443 | 443 | 443 | 443 | 256 | +| getSlasher | 496 | 496 | 496 | 496 | 518 | +| getSlotDuration | 421 | 421 | 421 | 421 | 256 | | getSpecificProverRewardsForEpoch | 822 | 2130 | 1634 | 3634 | 5 | | getTargetCommitteeSize | 462 | 462 | 462 | 462 | 768 | -| getTimestampForSlot | 909 | 910 | 909 | 4909 | 2462 | -| propose | 129177 | 377901 | 379855 | 589127 | 2601 | -| prune | 25642 | 36116 | 37377 | 41862 | 6 | -| setProvingCostPerMana | 28713 | 28713 | 28713 | 28713 | 2 | -| setupEpoch | 208063 | 1372704 | 1400001 | 1400001 | 262 | -| submitEpochRootProof | 64866 | 426856 | 424623 | 456356 | 882 | +| getTimestampForSlot | 887 | 888 | 887 | 4887 | 2462 | +| propose | 129177 | 378015 | 379855 | 589127 | 2601 | +| prune | 25731 | 36205 | 37466 | 41951 | 6 | +| setProvingCostPerMana | 28691 | 28691 | 28691 | 28691 | 2 | +| setupEpoch | 208152 | 1372793 | 1400090 | 1400090 | 262 | +| submitEpochRootProof | 64866 | 426881 | 424689 | 456422 | 873 | | src/core/messagebridge/Inbox.sol:Inbox contract | | | | | | |-------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | @@ -62,12 +62,12 @@ | src/core/messagebridge/Outbox.sol:Outbox contract | | | | | | |---------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | -| 586661 | 2646 | | | | | +| 586673 | 2646 | | | | | | Function Name | min | avg | median | max | # calls | -| consume | 28894 | 72056 | 73128 | 73400 | 4766 | -| getRootData | 940 | 1363 | 1171 | 3217 | 2732 | +| consume | 28894 | 72075 | 73128 | 73400 | 4705 | +| getRootData | 940 | 1343 | 1149 | 3217 | 2733 | | hasMessageBeenConsumedAtBlockAndIndex | 591 | 2583 | 2591 | 2591 | 259 | -| insert | 22188 | 57557 | 68264 | 68264 | 1102 | +| insert | 22188 | 57527 | 68264 | 68264 | 1099 | | src/core/staking/Slasher.sol:Slasher contract | | | | | | |-----------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | @@ -80,15 +80,15 @@ | Deployment Cost | Deployment Size | | | | | | 0 | 0 | | | | | | Function Name | min | avg | median | max | # calls | -| executeProposal | 139987 | 139987 | 139987 | 139987 | 1 | -| vote | 63907 | 75274 | 63907 | 120742 | 10 | +| executeProposal | 140075 | 140075 | 140075 | 140075 | 1 | +| vote | 64017 | 75384 | 64017 | 120852 | 10 | | src/governance/CoinIssuer.sol:CoinIssuer contract | | | | | | |---------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | -| 326373 | 1465 | | | | | +| 326385 | 1465 | | | | | | Function Name | min | avg | median | max | # calls | | RATE | 239 | 239 | 239 | 239 | 768 | -| mint | 23901 | 43841 | 26637 | 81105 | 768 | +| mint | 23901 | 43850 | 26637 | 81131 | 768 | | mintAvailable | 503 | 503 | 503 | 503 | 1283 | | timeOfLastMint | 360 | 360 | 360 | 360 | 256 | | src/governance/Governance.sol:Governance contract | | | | | | @@ -96,25 +96,25 @@ | Deployment Cost | Deployment Size | | | | | | 2332350 | 10841 | | | | | | Function Name | min | avg | median | max | # calls | -| deposit | 27898 | 171751 | 186529 | 188452 | 9729 | +| deposit | 27965 | 171786 | 186596 | 188519 | 9729 | | dropProposal | 23739 | 40533 | 33600 | 63600 | 2307 | -| execute | 26209 | 71290 | 71327 | 161717 | 3076 | -| finaliseWithdraw | 23757 | 45175 | 48283 | 65383 | 6072 | +| execute | 26209 | 71295 | 71327 | 161717 | 3076 | +| finaliseWithdraw | 23757 | 45165 | 48283 | 65383 | 6093 | | getConfiguration | 1913 | 12163 | 19913 | 19913 | 5396 | | getProposal | 3523 | 8023 | 3523 | 31523 | 10590 | | getProposalState | 469 | 11470 | 13558 | 21242 | 23311 | -| getWithdrawal | 1075 | 1075 | 1075 | 1075 | 10175 | +| getWithdrawal | 1075 | 1075 | 1075 | 1075 | 10202 | | governanceProposer | 424 | 1418 | 424 | 2424 | 515 | -| initiateWithdraw | 30945 | 199329 | 211342 | 228958 | 7596 | -| powerAt | 1042 | 1412 | 1042 | 3712 | 4608 | +| initiateWithdraw | 30945 | 199307 | 211342 | 228958 | 7601 | +| powerAt | 1042 | 1412 | 1042 | 3029 | 4608 | | proposalCount | 338 | 1714 | 2338 | 2338 | 1116 | -| propose | 23763 | 321926 | 320487 | 337587 | 606 | -| proposeWithLock | 26545 | 421002 | 422627 | 422627 | 257 | -| totalPowerAt | 612 | 1564 | 883 | 3568 | 6107 | -| updateConfiguration | 23457 | 32910 | 24180 | 48186 | 6145 | -| updateGovernanceProposer | 21705 | 27184 | 28016 | 28028 | 2048 | +| propose | 23763 | 321925 | 320487 | 337587 | 606 | +| proposeWithLock | 26545 | 421001 | 422627 | 422627 | 257 | +| totalPowerAt | 612 | 1567 | 883 | 3568 | 6083 | +| updateConfiguration | 23457 | 32912 | 24180 | 48186 | 6145 | +| updateGovernanceProposer | 21693 | 27183 | 28016 | 28028 | 2048 | | vote | 30670 | 87818 | 94478 | 94500 | 12289 | -| withdrawalCount | 383 | 390 | 383 | 2383 | 2522 | +| withdrawalCount | 383 | 391 | 383 | 2383 | 2498 | | src/governance/Registry.sol:Registry contract | | | | | | |-----------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | @@ -122,21 +122,21 @@ | Function Name | min | avg | median | max | # calls | | getCurrentSnapshot | 664 | 2664 | 2664 | 4664 | 514 | | getGovernance | 341 | 2159 | 2341 | 2341 | 2829 | -| getRollup | 374 | 2358 | 2374 | 2374 | 872089 | +| getRollup | 374 | 2358 | 2374 | 2374 | 873549 | | getSnapshot | 4740 | 4740 | 4740 | 4740 | 257 | | getVersionFor | 743 | 3527 | 2927 | 4927 | 773 | | isRollupRegistered | 657 | 3805 | 2812 | 4812 | 515 | | numberOfVersions | 350 | 1685 | 2350 | 2350 | 770 | | transferOwnership | 28592 | 28592 | 28592 | 28592 | 106 | -| upgrade | 23672 | 103297 | 106801 | 106801 | 6148 | +| upgrade | 23672 | 103310 | 106801 | 106801 | 6171 | | src/governance/RewardDistributor.sol:RewardDistributor contract | | | | | | |-----------------------------------------------------------------|-----------------|-------|--------|-------|---------| | Deployment Cost | Deployment Size | | | | | | 513664 | 2360 | | | | | | Function Name | min | avg | median | max | # calls | -| BLOCK_REWARD | 238 | 238 | 238 | 238 | 374 | -| canonicalRollup | 1143 | 3143 | 3143 | 5643 | 877 | -| claim | 30122 | 45805 | 35599 | 64024 | 513 | +| BLOCK_REWARD | 238 | 238 | 238 | 238 | 378 | +| canonicalRollup | 1143 | 3143 | 3143 | 5643 | 868 | +| claim | 30122 | 45875 | 35665 | 64090 | 513 | | owner | 2384 | 2384 | 2384 | 2384 | 257 | | registry | 347 | 1347 | 1347 | 2347 | 2 | | updateRegistry | 23757 | 23781 | 23757 | 30119 | 257 | @@ -150,15 +150,15 @@ | N | 260 | 260 | 260 | 260 | 1949 | | REGISTRY | 205 | 205 | 205 | 205 | 256 | | computeRound | 435 | 435 | 435 | 435 | 266 | -| executeProposal | 29491 | 43507 | 37213 | 366375 | 2053 | +| executeProposal | 29491 | 43507 | 37213 | 366485 | 2053 | | getExecutor | 3397 | 3397 | 3397 | 3397 | 256 | | getInstance | 951 | 951 | 951 | 951 | 256 | | rounds | 865 | 865 | 865 | 865 | 522 | -| vote | 29794 | 50139 | 50074 | 125975 | 858007 | +| vote | 29794 | 50139 | 50074 | 126085 | 859503 | | yeaCount | 851 | 851 | 851 | 851 | 16 | | src/periphery/Forwarder.sol:Forwarder contract | | | | | | |------------------------------------------------|-----------------|-------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | | 358690 | 1553 | | | | | | Function Name | min | avg | median | max | # calls | -| forward | 24936 | 27197 | 27012 | 132931 | 514 | +| forward | 24936 | 27197 | 27012 | 132983 | 514 | diff --git a/l1-contracts/src/core/RollupCore.sol b/l1-contracts/src/core/RollupCore.sol index b5238655d73d..36f5cf25cd99 100644 --- a/l1-contracts/src/core/RollupCore.sol +++ b/l1-contracts/src/core/RollupCore.sol @@ -126,6 +126,11 @@ contract RollupCore is CheatLib.setProtocolContractTreeRoot(_protocolContractTreeRoot); } + function updateManaTarget(uint256 _manaTarget) external override(ITestRollup) onlyOwner { + FeeLib.updateManaTarget(_manaTarget); + emit ITestRollup.ManaTargetUpdated(_manaTarget); + } + /* -------------------------------------------------------------------------- */ /* CHEAT CODES END HERE */ /* -------------------------------------------------------------------------- */ diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 2b6f5209608d..6eebd2fff93a 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -122,10 +122,13 @@ struct CheatDepositArgs { } interface ITestRollup { + event ManaTargetUpdated(uint256 indexed manaTarget); + function setEpochVerifier(address _verifier) external; function setVkTreeRoot(bytes32 _vkTreeRoot) external; function setProtocolContractTreeRoot(bytes32 _protocolContractTreeRoot) external; function cheat__InitialiseValidatorSet(CheatDepositArgs[] memory _args) external; + function updateManaTarget(uint256 _manaTarget) external; } interface IRollupCore { diff --git a/l1-contracts/src/core/libraries/rollup/FeeLib.sol b/l1-contracts/src/core/libraries/rollup/FeeLib.sol index 26825b80e721..ace726e7b0ed 100644 --- a/l1-contracts/src/core/libraries/rollup/FeeLib.sol +++ b/l1-contracts/src/core/libraries/rollup/FeeLib.sol @@ -156,6 +156,13 @@ library FeeLib { }); } + function updateManaTarget(uint256 _manaTarget) internal { + FeeStore storage feeStore = getStorage(); + feeStore.manaTarget = _manaTarget; + feeStore.congestionUpdateFraction = + _manaTarget * MAGIC_CONGESTION_VALUE_MULTIPLIER / MAGIC_CONGESTION_VALUE_DIVISOR; + } + function writeFeeHeader( uint256 _blockNumber, int256 _feeAssetPriceModifier, diff --git a/l1-contracts/src/governance/interfaces/IMintableERC20.sol b/l1-contracts/src/governance/interfaces/IMintableERC20.sol index f2bd66ffa877..5a4214087be1 100644 --- a/l1-contracts/src/governance/interfaces/IMintableERC20.sol +++ b/l1-contracts/src/governance/interfaces/IMintableERC20.sol @@ -5,5 +5,12 @@ pragma solidity >=0.8.27; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; interface IMintableERC20 is IERC20 { + event MinterAdded(address indexed minter); + event MinterRemoved(address indexed minter); + + error NotMinter(address caller); + function mint(address _to, uint256 _amount) external; + function addMinter(address _minter) external; + function removeMinter(address _minter) external; } diff --git a/l1-contracts/src/mock/FeeAssetHandler.sol b/l1-contracts/src/mock/FeeAssetHandler.sol new file mode 100644 index 000000000000..2b62ce552ba2 --- /dev/null +++ b/l1-contracts/src/mock/FeeAssetHandler.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {Ownable} from "@oz/access/Ownable.sol"; +import {IMintableERC20} from "./../governance/interfaces/IMintableERC20.sol"; + +interface IFeeAssetHandler { + event MintAmountSet(uint256 amount); + + function mint(address _recipient) external; + function setMintAmount(uint256 _amount) external; +} + +contract FeeAssetHandler is IFeeAssetHandler, Ownable { + IMintableERC20 public immutable FEE_ASSET; + uint256 public mintAmount; + + constructor(address _owner, address _feeAsset, uint256 _mintAmount) Ownable(_owner) { + FEE_ASSET = IMintableERC20(_feeAsset); + mintAmount = _mintAmount; + } + + function mint(address _recipient) external override { + FEE_ASSET.mint(_recipient, mintAmount); + } + + function setMintAmount(uint256 _amount) external override onlyOwner { + mintAmount = _amount; + emit MintAmountSet(_amount); + } +} diff --git a/l1-contracts/src/mock/TestERC20.sol b/l1-contracts/src/mock/TestERC20.sol index 3883b407347d..594a12f9ebc1 100644 --- a/l1-contracts/src/mock/TestERC20.sol +++ b/l1-contracts/src/mock/TestERC20.sol @@ -7,27 +7,42 @@ import {ERC20} from "@oz/token/ERC20/ERC20.sol"; import {IMintableERC20} from "./../governance/interfaces/IMintableERC20.sol"; contract TestERC20 is ERC20, IMintableERC20, Ownable { - bool public freeForAll = false; + mapping(address => bool) public minters; - modifier ownerOrFreeForAll() { - if (msg.sender != owner() && !freeForAll) { - revert("Not owner or free for all"); - } + modifier onlyMinter() { + require(minters[msg.sender], NotMinter(msg.sender)); _; } constructor(string memory _name, string memory _symbol, address _owner) ERC20(_name, _symbol) Ownable(_owner) - {} - - // solhint-disable-next-line comprehensive-interface - function setFreeForAll(bool _freeForAll) external onlyOwner { - freeForAll = _freeForAll; + { + minters[_owner] = true; + emit MinterAdded(_owner); } - function mint(address _to, uint256 _amount) external override(IMintableERC20) ownerOrFreeForAll { + function mint(address _to, uint256 _amount) external override(IMintableERC20) onlyMinter { _mint(_to, _amount); } + + function addMinter(address _minter) public override(IMintableERC20) onlyOwner { + minters[_minter] = true; + emit MinterAdded(_minter); + } + + function removeMinter(address _minter) public override(IMintableERC20) onlyOwner { + minters[_minter] = false; + emit MinterRemoved(_minter); + } + + function transferOwnership(address newOwner) public override(Ownable) onlyOwner { + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } + removeMinter(owner()); + addMinter(newOwner); + _transferOwnership(newOwner); + } } // docs:end:contract diff --git a/l1-contracts/test/TestERC20.t.sol b/l1-contracts/test/TestERC20.t.sol deleted file mode 100644 index b95dcd9d49d5..000000000000 --- a/l1-contracts/test/TestERC20.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -pragma solidity ^0.8.18; - -import "forge-std/Test.sol"; -import {TestERC20} from "@aztec/mock/TestERC20.sol"; - -contract TestERC20Test is Test { - TestERC20 testERC20; - - function setUp() public { - testERC20 = new TestERC20("test", "TEST", address(this)); - } - - function test_mint() public { - testERC20.mint(address(this), 100); - assertEq(testERC20.balanceOf(address(this)), 100); - } - - function test_mint_only_owner(address _caller) public { - vm.assume(_caller != address(this)); - vm.expectRevert(); - vm.prank(_caller); - testERC20.mint(address(this), 100); - } -} diff --git a/l1-contracts/test/fee_asset_handler/base.t.sol b/l1-contracts/test/fee_asset_handler/base.t.sol new file mode 100644 index 000000000000..6a25126732bc --- /dev/null +++ b/l1-contracts/test/fee_asset_handler/base.t.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {TestERC20} from "@aztec/mock/TestERC20.sol"; +import {FeeAssetHandler} from "@aztec/mock/FeeAssetHandler.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract FeeAssetHandlerTestBase is Test { + TestERC20 internal testERC20; + FeeAssetHandler internal feeAssetHandler; + + function setUp() external { + testERC20 = new TestERC20("test", "TEST", address(this)); + feeAssetHandler = new FeeAssetHandler(address(this), address(testERC20), 100); + testERC20.addMinter(address(feeAssetHandler)); + } +} diff --git a/l1-contracts/test/fee_asset_handler/mint.t.sol b/l1-contracts/test/fee_asset_handler/mint.t.sol new file mode 100644 index 000000000000..fb41ae577f3b --- /dev/null +++ b/l1-contracts/test/fee_asset_handler/mint.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IERC20} from "@oz/token/ERC20/ERC20.sol"; +import {FeeAssetHandlerTestBase} from "./base.t.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract MintTest is FeeAssetHandlerTestBase { + function test_WhenAnyoneCallsMint(address _caller, address _recipient) external { + // it mints the mint amount to the recipient + // it emits {Minted} event + vm.assume(_recipient != address(0)); + vm.expectEmit(true, true, true, true, address(testERC20)); + emit IERC20.Transfer(address(0), _recipient, feeAssetHandler.mintAmount()); + vm.prank(_caller); + feeAssetHandler.mint(_recipient); + assertEq(testERC20.balanceOf(_recipient), feeAssetHandler.mintAmount()); + } +} diff --git a/l1-contracts/test/fee_asset_handler/mint.tree b/l1-contracts/test/fee_asset_handler/mint.tree new file mode 100644 index 000000000000..ec4707007544 --- /dev/null +++ b/l1-contracts/test/fee_asset_handler/mint.tree @@ -0,0 +1,4 @@ +MintTest +└── when anyone calls mint + ├── it mints the mint amount to the recipient + └── it emits {Minted} event diff --git a/l1-contracts/test/fee_asset_handler/setMintAmount.t.sol b/l1-contracts/test/fee_asset_handler/setMintAmount.t.sol new file mode 100644 index 000000000000..57ca2ccbc972 --- /dev/null +++ b/l1-contracts/test/fee_asset_handler/setMintAmount.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IFeeAssetHandler} from "@aztec/mock/FeeAssetHandler.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; +import {FeeAssetHandlerTestBase} from "./base.t.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract SetMintAmountTest is FeeAssetHandlerTestBase { + function test_WhenCallerIsNotOwner(address _caller, uint256 _mintAmount) external { + // it reverts + vm.assume(_caller != feeAssetHandler.owner()); + vm.prank(_caller); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + feeAssetHandler.setMintAmount(_mintAmount); + } + + function test_WhenCallerIsOwner(uint256 _mintAmount) external { + // it updates the mint amount + // it emits {MintAmountUpdated} event + vm.expectEmit(true, true, true, true, address(feeAssetHandler)); + emit IFeeAssetHandler.MintAmountSet(_mintAmount); + feeAssetHandler.setMintAmount(_mintAmount); + assertEq(feeAssetHandler.mintAmount(), _mintAmount); + } +} diff --git a/l1-contracts/test/fee_asset_handler/setMintAmount.tree b/l1-contracts/test/fee_asset_handler/setMintAmount.tree new file mode 100644 index 000000000000..330963dba882 --- /dev/null +++ b/l1-contracts/test/fee_asset_handler/setMintAmount.tree @@ -0,0 +1,6 @@ +SetMintAmountTest +├── when caller is not owner +│ └── it reverts +└── when caller is owner + ├── it updates the mint amount + └── it emits {MintAmountUpdated} event diff --git a/l1-contracts/test/test_erc20/addMinter.t.sol b/l1-contracts/test/test_erc20/addMinter.t.sol new file mode 100644 index 000000000000..ce790c6b3844 --- /dev/null +++ b/l1-contracts/test/test_erc20/addMinter.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; +import {TestERC20TestBase} from "./base.t.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract AddMinterTest is TestERC20TestBase { + modifier whenTheCallerIsTheOwner() { + vm.startPrank(testERC20.owner()); + _; + vm.stopPrank(); + } + + modifier whenTheCallerIsNotTheOwner(address _caller) { + vm.assume(_caller != testERC20.owner()); + vm.startPrank(_caller); + _; + vm.stopPrank(); + } + + function test_WhenTheCallerIsNotTheOwner(address _caller, address _minter) + external + whenTheCallerIsNotTheOwner(_caller) + { + // it reverts + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + testERC20.addMinter(_minter); + } + + function test_WhenTheCallerIsTheOwner(address _minter) external whenTheCallerIsTheOwner { + // it adds the minter + // it emits a MinterAdded event + vm.expectEmit(true, true, true, true, address(testERC20)); + emit IMintableERC20.MinterAdded(_minter); + testERC20.addMinter(_minter); + assertEq(testERC20.minters(_minter), true); + } +} diff --git a/l1-contracts/test/test_erc20/addMinter.tree b/l1-contracts/test/test_erc20/addMinter.tree new file mode 100644 index 000000000000..fb02c2b85e87 --- /dev/null +++ b/l1-contracts/test/test_erc20/addMinter.tree @@ -0,0 +1,6 @@ +AddMinterTest +├── when the caller is not the owner +│ └── it reverts +└── when the caller is the owner + ├── it adds the minter + └── it emits a MinterAdded event diff --git a/l1-contracts/test/test_erc20/base.t.sol b/l1-contracts/test/test_erc20/base.t.sol new file mode 100644 index 000000000000..ca97b9bebbb5 --- /dev/null +++ b/l1-contracts/test/test_erc20/base.t.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {Test} from "forge-std/Test.sol"; +import {TestERC20} from "@aztec/mock/TestERC20.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract TestERC20TestBase is Test { + // solhint-disable private-vars-leading-underscore + TestERC20 internal testERC20; + + function setUp() external { + testERC20 = new TestERC20("test", "TEST", address(this)); + } +} diff --git a/l1-contracts/test/test_erc20/mint.t.sol b/l1-contracts/test/test_erc20/mint.t.sol new file mode 100644 index 000000000000..280f82475c02 --- /dev/null +++ b/l1-contracts/test/test_erc20/mint.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol"; +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {TestERC20TestBase} from "./base.t.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract MintTest is TestERC20TestBase { + function test_WhenTheCallerIsNotAMinter(address _caller, address _to, uint256 _amount) external { + vm.assume(_caller != testERC20.owner()); + vm.startPrank(_caller); + // it reverts + vm.expectRevert(abi.encodeWithSelector(IMintableERC20.NotMinter.selector, _caller)); + testERC20.mint(_to, _amount); + vm.stopPrank(); + } + + function test_WhenTheCallerIsAMinter(address _caller, address _to, uint256 _amount) external { + // it mints the amount to _to + // it emits a Transfer event + vm.prank(testERC20.owner()); + testERC20.addMinter(_caller); + + vm.assume(_to != address(0)); + + vm.prank(_caller); + vm.expectEmit(true, true, true, true, address(testERC20)); + emit IERC20.Transfer(address(0), _to, _amount); + testERC20.mint(_to, _amount); + } +} diff --git a/l1-contracts/test/test_erc20/mint.tree b/l1-contracts/test/test_erc20/mint.tree new file mode 100644 index 000000000000..eb9688bea80b --- /dev/null +++ b/l1-contracts/test/test_erc20/mint.tree @@ -0,0 +1,6 @@ +MintTest +├── when the caller is not a minter +│ └── it reverts +└── when the caller is a minter + ├── it mints the amount to _to + └── it emits a Transfer event diff --git a/l1-contracts/test/test_erc20/removeMinter.t.sol b/l1-contracts/test/test_erc20/removeMinter.t.sol new file mode 100644 index 000000000000..42c9655e9344 --- /dev/null +++ b/l1-contracts/test/test_erc20/removeMinter.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {IMintableERC20} from "@aztec/governance/interfaces/IMintableERC20.sol"; +import {Ownable} from "@oz/access/Ownable.sol"; +import {TestERC20TestBase} from "./base.t.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract RemoveMinterTest is TestERC20TestBase { + modifier whenTheCallerIsTheOwner() { + vm.startPrank(testERC20.owner()); + _; + vm.stopPrank(); + } + + modifier whenTheCallerIsNotTheOwner(address _caller) { + vm.assume(_caller != testERC20.owner()); + vm.startPrank(_caller); + _; + vm.stopPrank(); + } + + function test_WhenTheCallerIsNotTheOwner(address _caller, address _minter) + external + whenTheCallerIsNotTheOwner(_caller) + { + // it reverts + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + testERC20.removeMinter(_minter); + } + + function test_WhenTheCallerIsTheOwner(address _minter) external whenTheCallerIsTheOwner { + // it removes the minter + // it emits a MinterRemoved event + vm.expectEmit(true, true, true, true, address(testERC20)); + emit IMintableERC20.MinterRemoved(_minter); + testERC20.removeMinter(_minter); + assertEq(testERC20.minters(_minter), false); + } +} diff --git a/l1-contracts/test/test_erc20/removeMinter.tree b/l1-contracts/test/test_erc20/removeMinter.tree new file mode 100644 index 000000000000..d22e694016f0 --- /dev/null +++ b/l1-contracts/test/test_erc20/removeMinter.tree @@ -0,0 +1,7 @@ +RemoveMinterTest +├── when the caller is not the owner +│ └── it reverts +└── when the caller is the owner + ├── it removes the minter + └── it emits a MinterRemoved event + diff --git a/l1-contracts/test/test_erc20/transferOwnership.t.sol b/l1-contracts/test/test_erc20/transferOwnership.t.sol new file mode 100644 index 000000000000..a5101b780777 --- /dev/null +++ b/l1-contracts/test/test_erc20/transferOwnership.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.27; + +import {Ownable} from "@oz/access/Ownable.sol"; + +import {TestERC20TestBase} from "./base.t.sol"; + +// solhint-disable comprehensive-interface +// solhint-disable func-name-mixedcase + +contract TransferOwnershipTest is TestERC20TestBase { + modifier whenTheCallerIsNotTheOwner(address _caller) { + vm.assume(_caller != testERC20.owner()); + vm.startPrank(_caller); + _; + vm.stopPrank(); + } + + function test_WhenTheCallerIsNotTheOwner(address _caller, address _newOwner) + external + whenTheCallerIsNotTheOwner(_caller) + { + // it reverts + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _caller)); + testERC20.transferOwnership(_newOwner); + } + + // solhint-disable-next-line ordering + modifier whenTheCallerIsTheOwner() { + vm.startPrank(testERC20.owner()); + _; + vm.stopPrank(); + } + + function test_WhenTheNewOwnerIsTheZeroAddress() external whenTheCallerIsTheOwner { + // it reverts + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0))); + testERC20.transferOwnership(address(0)); + } + + function test_WhenTheNewOwnerIsNotTheZeroAddress(address _newOwner) + external + whenTheCallerIsTheOwner + { + // it removes the old owner as a minter + // it adds the new owner as a minter + // it transfers the ownership to the new owner + // it emits a OwnershipTransferred event + address oldOwner = testERC20.owner(); + + vm.assume(_newOwner != address(0)); + vm.expectEmit(true, true, true, true, address(testERC20)); + emit Ownable.OwnershipTransferred(testERC20.owner(), _newOwner); + testERC20.transferOwnership(_newOwner); + + assertEq(testERC20.owner(), _newOwner); + assertEq(testERC20.minters(_newOwner), true); + if (_newOwner != oldOwner) { + assertEq(testERC20.minters(oldOwner), false); + } + } +} diff --git a/l1-contracts/test/test_erc20/transferOwnership.tree b/l1-contracts/test/test_erc20/transferOwnership.tree new file mode 100644 index 000000000000..7c9114cb745d --- /dev/null +++ b/l1-contracts/test/test_erc20/transferOwnership.tree @@ -0,0 +1,12 @@ +TransferOwnershipTest +├── when the caller is not the owner +│ └── it reverts +└── when the caller is the owner + ├── when the new owner is the zero address + │ └── it reverts + └── when the new owner is not the zero address + ├── it removes the old owner as a minter + ├── it adds the new owner as a minter + ├── it transfers the ownership to the new owner + └── it emits a OwnershipTransferred event + diff --git a/spartan/aztec-network/files/config/config-full-node-env.sh b/spartan/aztec-network/files/config/config-full-node-env.sh index 4c0e03c68773..cd6b6b14dad3 100644 --- a/spartan/aztec-network/files/config/config-full-node-env.sh +++ b/spartan/aztec-network/files/config/config-full-node-env.sh @@ -7,6 +7,7 @@ if [ -n "$REGISTRY_CONTRACT_ADDRESS" ] && [ -n "$BOOTSTRAP_NODES" ]; then export BOOTSTRAP_NODES=$BOOTSTRAP_NODES export REGISTRY_CONTRACT_ADDRESS=$REGISTRY_CONTRACT_ADDRESS export SLASH_FACTORY_CONTRACT_ADDRESS=$SLASH_FACTORY_CONTRACT_ADDRESS +export FEE_ASSET_HANDLER_CONTRACT_ADDRESS=$FEE_ASSET_HANDLER_CONTRACT_ADDRESS EOF cat /shared/contracts/contracts.env exit 0 @@ -25,12 +26,14 @@ if [ "$P2P_ENABLED" = "true" ]; then fi registry_address=$(echo "$output" | grep -oP 'Registry Address: \K0x[a-fA-F0-9]{40}') slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}') +fee_asset_handler_address=$(echo "$output" | grep -oP 'FeeAssetHandler Address: \K0x[a-fA-F0-9]{40}') # Write the addresses to a file in the shared volume cat </shared/contracts/contracts.env export BOOTSTRAP_NODES=$boot_node_enr export REGISTRY_CONTRACT_ADDRESS=$registry_address export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address +export FEE_ASSET_HANDLER_CONTRACT_ADDRESS=$fee_asset_handler_address EOF cat /shared/contracts/contracts.env diff --git a/spartan/aztec-network/files/config/deploy-l1-contracts.sh b/spartan/aztec-network/files/config/deploy-l1-contracts.sh index bf99ebff4947..0b63396587cc 100755 --- a/spartan/aztec-network/files/config/deploy-l1-contracts.sh +++ b/spartan/aztec-network/files/config/deploy-l1-contracts.sh @@ -3,10 +3,23 @@ set -exu # If REGISTRY_CONTRACT_ADDRESS is already set, skip deployment and just write the file if [ -n "${REGISTRY_CONTRACT_ADDRESS:-}" ]; then + # make sure that the fee asset handler address is set + if [ -z "${FEE_ASSET_HANDLER_CONTRACT_ADDRESS:-}" ]; then + echo "Registry address is already set, but FeeAssetHandler address is not set. Exiting." + exit 1 + fi + # make sure the slash factory address is set + if [ -z "${SLASH_FACTORY_CONTRACT_ADDRESS:-}" ]; then + echo "Registry address is already set, but SlashFactory address is not set. Exiting." + exit 1 + fi + echo "Registry address already set. Skipping deployment." # Write the addresses to a file in the shared volume cat </shared/contracts/contracts.env export REGISTRY_CONTRACT_ADDRESS=$REGISTRY_CONTRACT_ADDRESS +export SLASH_FACTORY_CONTRACT_ADDRESS=$SLASH_FACTORY_CONTRACT_ADDRESS +export FEE_ASSET_HANDLER_CONTRACT_ADDRESS=$FEE_ASSET_HANDLER_CONTRACT_ADDRESS EOF cat /shared/contracts/contracts.env exit 0 @@ -72,11 +85,12 @@ echo "$output" # Extract contract addresses using grep and regex registry_address=$(echo "$output" | grep -oP 'Registry Address: \K0x[a-fA-F0-9]{40}') slash_factory_address=$(echo "$output" | grep -oP 'SlashFactory Address: \K0x[a-fA-F0-9]{40}') - +fee_asset_handler_address=$(echo "$output" | grep -oP 'FeeAssetHandler Address: \K0x[a-fA-F0-9]{40}') # Write the addresses to a file in the shared volume cat </shared/contracts/contracts.env export REGISTRY_CONTRACT_ADDRESS=$registry_address export SLASH_FACTORY_CONTRACT_ADDRESS=$slash_factory_address +export FEE_ASSET_HANDLER_CONTRACT_ADDRESS=$fee_asset_handler_address EOF cat /shared/contracts/contracts.env diff --git a/yarn-project/aztec.js/src/ethereum/portal_manager.ts b/yarn-project/aztec.js/src/ethereum/portal_manager.ts index fb644aeebaef..c4cdcb0465cc 100644 --- a/yarn-project/aztec.js/src/ethereum/portal_manager.ts +++ b/yarn-project/aztec.js/src/ethereum/portal_manager.ts @@ -5,6 +5,7 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import type { Logger } from '@aztec/foundation/log'; import type { SiblingPath } from '@aztec/foundation/trees'; +import { FeeAssetHandlerAbi } from '@aztec/l1-artifacts/FeeAssetHandlerAbi'; import { FeeJuicePortalAbi } from '@aztec/l1-artifacts/FeeJuicePortalAbi'; import { OutboxAbi } from '@aztec/l1-artifacts/OutboxAbi'; import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi'; @@ -57,19 +58,39 @@ export async function generateClaimSecret(logger?: Logger): Promise<[Fr, Fr]> { /** Helper for managing an ERC20 on L1. */ export class L1TokenManager { private contract: GetContractReturnType; + private handler: GetContractReturnType | undefined; public constructor( /** Address of the ERC20 contract. */ - public readonly address: EthAddress, + public readonly tokenAddress: EthAddress, + /** Address of the handler/faucet contract. */ + public readonly handlerAddress: EthAddress | undefined, private publicClient: ViemPublicClient, private walletClient: ViemWalletClient, private logger: Logger, ) { this.contract = getContract({ - address: this.address.toString(), + address: this.tokenAddress.toString(), abi: TestERC20Abi, client: this.walletClient, }); + if (this.handlerAddress) { + this.handler = getContract({ + address: this.handlerAddress.toString(), + abi: FeeAssetHandlerAbi, + client: this.walletClient, + }); + } + } + + /** Returns the amount of tokens available to mint via the handler. + * @throws if the handler is not provided. + */ + public async getMintAmount() { + if (!this.handler) { + throw new Error('Minting handler was not provided'); + } + return await this.handler.read.mintAmount(); } /** @@ -81,16 +102,18 @@ export class L1TokenManager { } /** - * Mints tokens for the given address. Returns once the tx has been mined. - * @param amount - Amount to mint. + * Mints a fixed amount of tokens for the given address. Returns once the tx has been mined. * @param address - Address to mint the tokens for. * @param addressName - Optional name of the address for logging. */ - public async mint(amount: bigint, address: Hex, addressName?: string) { - this.logger.info(`Minting ${amount} tokens for ${stringifyEthAddress(address, addressName)}`); - await this.publicClient.waitForTransactionReceipt({ - hash: await this.contract.write.mint([address, amount]), - }); + public async mint(address: Hex, addressName?: string) { + if (!this.handler) { + throw new Error('Minting handler was not provided'); + } + const mintAmount = await this.getMintAmount(); + this.logger.info(`Minting ${mintAmount} tokens for ${stringifyEthAddress(address, addressName)}`); + // NOTE: the handler mints a fixed amount. + await this.handler.write.mint([address]); } /** @@ -115,11 +138,12 @@ export class L1FeeJuicePortalManager { constructor( portalAddress: EthAddress, tokenAddress: EthAddress, + handlerAddress: EthAddress, private readonly publicClient: ViemPublicClient, private readonly walletClient: ViemWalletClient, private readonly logger: Logger, ) { - this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger); + this.tokenManager = new L1TokenManager(tokenAddress, handlerAddress, publicClient, walletClient, logger); this.contract = getContract({ address: portalAddress.toString(), abi: FeeJuicePortalAbi, @@ -138,16 +162,21 @@ export class L1FeeJuicePortalManager { * @param amount - Amount of tokens to send. * @param mint - Whether to mint the tokens before sending (only during testing). */ - public async bridgeTokensPublic(to: AztecAddress, amount: bigint, mint = false): Promise { + public async bridgeTokensPublic(to: AztecAddress, amount: bigint | undefined, mint = false): Promise { const [claimSecret, claimSecretHash] = await generateClaimSecret(); + const mintableAmount = await this.tokenManager.getMintAmount(); + const amountToBridge = amount ?? mintableAmount; if (mint) { - await this.tokenManager.mint(amount, this.walletClient.account.address); + if (amountToBridge !== mintableAmount) { + throw new Error(`Minting amount must be ${mintableAmount}`); + } + await this.tokenManager.mint(this.walletClient.account.address); } - await this.tokenManager.approve(amount, this.contract.address, 'FeeJuice Portal'); + await this.tokenManager.approve(amountToBridge, this.contract.address, 'FeeJuice Portal'); this.logger.info('Sending L1 Fee Juice to L2 to be claimed publicly'); - const args = [to.toString(), amount, claimSecretHash.toString()] as const; + const args = [to.toString(), amountToBridge, claimSecretHash.toString()] as const; await this.contract.simulate.depositToAztecPublic(args); @@ -164,13 +193,13 @@ export class L1FeeJuicePortalManager { 'DepositToAztecPublic', log => log.args.secretHash === claimSecretHash.toString() && - log.args.amount === amount && + log.args.amount === amountToBridge && log.args.to === to.toString(), this.logger, ); return { - claimAmount: amount, + claimAmount: amountToBridge, claimSecret, claimSecretHash, messageHash: log.args.key, @@ -192,14 +221,24 @@ export class L1FeeJuicePortalManager { logger: Logger, ): Promise { const { - l1ContractAddresses: { feeJuiceAddress, feeJuicePortalAddress }, + l1ContractAddresses: { feeJuiceAddress, feeJuicePortalAddress, feeAssetHandlerAddress }, } = await walletOrPxe.getNodeInfo(); if (feeJuiceAddress.isZero() || feeJuicePortalAddress.isZero()) { throw new Error('Portal or token not deployed on L1'); } + if (!feeAssetHandlerAddress || feeAssetHandlerAddress.isZero()) { + throw new Error('Handler not deployed on L1, or handler address is zero'); + } - return new L1FeeJuicePortalManager(feeJuicePortalAddress, feeJuiceAddress, publicClient, walletClient, logger); + return new L1FeeJuicePortalManager( + feeJuicePortalAddress, + feeJuiceAddress, + feeAssetHandlerAddress, + publicClient, + walletClient, + logger, + ); } } @@ -211,11 +250,12 @@ export class L1ToL2TokenPortalManager { constructor( portalAddress: EthAddress, tokenAddress: EthAddress, + handlerAddress: EthAddress | undefined, protected publicClient: ViemPublicClient, protected walletClient: ViemWalletClient, protected logger: Logger, ) { - this.tokenManager = new L1TokenManager(tokenAddress, publicClient, walletClient, logger); + this.tokenManager = new L1TokenManager(tokenAddress, handlerAddress, publicClient, walletClient, logger); this.portal = getContract({ address: portalAddress.toString(), abi: TokenPortalAbi, @@ -316,7 +356,11 @@ export class L1ToL2TokenPortalManager { private async bridgeSetup(amount: bigint, mint: boolean) { if (mint) { - await this.tokenManager.mint(amount, this.walletClient.account.address); + const mintableAmount = await this.tokenManager.getMintAmount(); + if (amount !== mintableAmount) { + throw new Error(`Minting amount must be ${mintableAmount} for testing`); + } + await this.tokenManager.mint(this.walletClient.account.address); } await this.tokenManager.approve(amount, this.portal.address, 'TokenPortal'); return generateClaimSecret(); @@ -330,12 +374,13 @@ export class L1TokenPortalManager extends L1ToL2TokenPortalManager { constructor( portalAddress: EthAddress, tokenAddress: EthAddress, + handlerAddress: EthAddress | undefined, outboxAddress: EthAddress, publicClient: ViemPublicClient, walletClient: ViemWalletClient, logger: Logger, ) { - super(portalAddress, tokenAddress, publicClient, walletClient, logger); + super(portalAddress, tokenAddress, handlerAddress, publicClient, walletClient, logger); this.outbox = getContract({ address: outboxAddress.toString(), abi: OutboxAbi, diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index 632e41138fb8..d642edad3bdd 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -101,7 +101,7 @@ export class BotFactory { const address = account.getAddress(); this.log.info(`Deploying account at ${address}`); - const claim = await this.bridgeL1FeeJuice(address, 10n ** 22n); + const claim = await this.bridgeL1FeeJuice(address); // docs:start:claim_and_deploy const wallet = await account.getWallet(); @@ -218,7 +218,7 @@ export class BotFactory { await sentTx.wait({ timeout: this.config.txMinedWaitSeconds }); } - private async bridgeL1FeeJuice(recipient: AztecAddress, amount: bigint) { + private async bridgeL1FeeJuice(recipient: AztecAddress) { const l1RpcUrls = this.config.l1RpcUrls; if (!l1RpcUrls?.length) { throw new Error('L1 Rpc url is required to bridge the fee juice to fund the deployment of the account.'); @@ -235,12 +235,13 @@ export class BotFactory { const { publicClient, walletClient } = createL1Clients(chain.rpcUrls, mnemonicOrPrivateKey, chain.chainInfo); const portal = await L1FeeJuicePortalManager.new(this.pxe, publicClient, walletClient, this.log); - const claim = await portal.bridgeTokensPublic(recipient, amount, true /* mint */); + const mintAmount = await portal.getTokenManager().getMintAmount(); + const claim = await portal.bridgeTokensPublic(recipient, mintAmount, true /* mint */); const isSynced = async () => await this.pxe.isL1ToL2MessageSynced(Fr.fromHexString(claim.messageHash)); await retryUntil(isSynced, `message ${claim.messageHash} sync`, 24, 1); - this.log.info(`Created a claim for ${amount} L1 fee juice to ${recipient}.`, claim); + this.log.info(`Created a claim for ${mintAmount} L1 fee juice to ${recipient}.`, claim); // Progress by 2 L2 blocks so that the l1ToL2Message added above will be available to use on L2. await this.advanceL2Block(); diff --git a/yarn-project/cli-wallet/src/cmds/index.ts b/yarn-project/cli-wallet/src/cmds/index.ts index 2025297ad09e..3e43c2ff6f0e 100644 --- a/yarn-project/cli-wallet/src/cmds/index.ts +++ b/yarn-project/cli-wallet/src/cmds/index.ts @@ -423,6 +423,7 @@ export function injectCommands( const { bridgeL1FeeJuice } = await import('./bridge_fee_juice.js'); const { rpcUrl, l1ChainId, l1RpcUrls, l1PrivateKey, mnemonic, mint, json, wait, interval: intervalS } = options; const client = pxeWrapper?.getPXE() ?? (await createCompatibleClient(rpcUrl, debugLogger)); + log(`Minting ${amount} fee juice on L1 and pushing to L2`); const [secret, messageLeafIndex] = await bridgeL1FeeJuice( amount, diff --git a/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh b/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh index c2fc0d290058..affbed54862c 100755 --- a/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh +++ b/yarn-project/cli-wallet/test/flows/create_account_pay_native.sh @@ -6,7 +6,7 @@ source shared/setup.sh test_title "Create an account and deploy using native fee payment with bridging" aztec-wallet create-account -a main --register-only -aztec-wallet bridge-fee-juice 100000000000000000 main --mint --no-wait +aztec-wallet bridge-fee-juice 1000000000000000000 main --mint --no-wait section "Use a pre-funded test account to send dummy txs to force block creations" diff --git a/yarn-project/cli-wallet/test/flows/shared/create_funded_account.sh b/yarn-project/cli-wallet/test/flows/shared/create_funded_account.sh index fbb8d752b2db..d75ac17b57ea 100644 --- a/yarn-project/cli-wallet/test/flows/shared/create_funded_account.sh +++ b/yarn-project/cli-wallet/test/flows/shared/create_funded_account.sh @@ -3,7 +3,7 @@ ALIAS=$1 section "Creating a funded account (alias: $ALIAS)" aztec-wallet create-account -a $ALIAS --register-only -aztec-wallet bridge-fee-juice 100000000000000000 $ALIAS --mint --no-wait +aztec-wallet bridge-fee-juice 1000000000000000000 $ALIAS --mint --no-wait # The following produces two blocks, allowing the claim to be used in the next block. source $flows/shared/deploy_token.sh tmp-token-$ALIAS $ALIAS diff --git a/yarn-project/cli-wallet/test/flows/shared/deploy_main_account_and_token.sh b/yarn-project/cli-wallet/test/flows/shared/deploy_main_account_and_token.sh index 475ed87f507f..cb17c657097e 100644 --- a/yarn-project/cli-wallet/test/flows/shared/deploy_main_account_and_token.sh +++ b/yarn-project/cli-wallet/test/flows/shared/deploy_main_account_and_token.sh @@ -4,7 +4,7 @@ ACCOUNT_ALIAS=main section "Deploying token contract (alias: $TOKEN_ALIAS) and creating a funded account (alias: $ACCOUNT_ALIAS)" aztec-wallet create-account -a $ACCOUNT_ALIAS --register-only -aztec-wallet bridge-fee-juice 100000000000000000 $ACCOUNT_ALIAS --mint --no-wait +aztec-wallet bridge-fee-juice 1000000000000000000 $ACCOUNT_ALIAS --mint --no-wait # Deploy token contract and set the main account as a minter. # The following produces two blocks, allowing the claim to be used in the next block. diff --git a/yarn-project/cli-wallet/test/flows/shared/deploy_sponsored_fpc_and_token.sh b/yarn-project/cli-wallet/test/flows/shared/deploy_sponsored_fpc_and_token.sh index 2e38d4f29972..e7f6e1858ffc 100644 --- a/yarn-project/cli-wallet/test/flows/shared/deploy_sponsored_fpc_and_token.sh +++ b/yarn-project/cli-wallet/test/flows/shared/deploy_sponsored_fpc_and_token.sh @@ -6,7 +6,7 @@ section "Deploying token contract (alias: $TOKEN_ALIAS) and creating a sponsored aztec-wallet import-test-accounts aztec-wallet deploy sponsored_fpc_contract@SponsoredFPC -f test0 -a $FPC_ALIAS --no-init -CLAIM=$(aztec-wallet bridge-fee-juice 100000000000000000 contracts:$FPC_ALIAS --mint --no-wait --json) +CLAIM=$(aztec-wallet bridge-fee-juice 1000000000000000000 contracts:$FPC_ALIAS --mint --no-wait --json) retrieve () { echo "$CLAIM" | grep "\"$1\"" | awk -F ': ' '{print $2}' | tr -d '",' diff --git a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts index 69f094dcb334..aff4e47b1b53 100644 --- a/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts +++ b/yarn-project/cli/src/cmds/devnet/bootstrap_network.ts @@ -13,7 +13,6 @@ import { retryUntil, waitForProven, } from '@aztec/aztec.js'; -import { FEE_FUNDING_FOR_TESTER_ACCOUNT } from '@aztec/constants'; import { type ContractArtifacts, type L1Clients, @@ -291,10 +290,9 @@ async function fundFPC( debugLog, ); - const amount = FEE_FUNDING_FOR_TESTER_ACCOUNT; const { claimAmount, claimSecret, messageLeafIndex, messageHash } = await feeJuicePortal.bridgeTokensPublic( fpcAddress, - amount, + undefined, true, ); diff --git a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts index 6f676b241b29..3b7764102e03 100644 --- a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts +++ b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts @@ -12,6 +12,7 @@ export async function bridgeERC20( privateKey: string | undefined, mnemonic: string, tokenAddress: EthAddress, + handlerAddress: EthAddress | undefined, portalAddress: EthAddress, privateTransfer: boolean, mint: boolean, @@ -24,7 +25,14 @@ export async function bridgeERC20( const { publicClient, walletClient } = createL1Clients(chain.rpcUrls, privateKey ?? mnemonic, chain.chainInfo); // Setup portal manager - const manager = new L1ToL2TokenPortalManager(portalAddress, tokenAddress, publicClient, walletClient, debugLogger); + const manager = new L1ToL2TokenPortalManager( + portalAddress, + tokenAddress, + handlerAddress, + publicClient, + walletClient, + debugLogger, + ); let claimSecret: Fr; let messageHash: `0x${string}`; if (privateTransfer) { diff --git a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts index 23d006155fd5..2e9b3a5f3231 100644 --- a/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts +++ b/yarn-project/cli/src/cmds/l1/deploy_l1_contracts.ts @@ -61,5 +61,6 @@ export async function deployL1Contracts( log(`GovernanceProposer Address: ${l1ContractAddresses.governanceProposerAddress.toString()}`); log(`Governance Address: ${l1ContractAddresses.governanceAddress.toString()}`); log(`SlashFactory Address: ${l1ContractAddresses.slashFactoryAddress?.toString()}`); + log(`FeeAssetHandler Address: ${l1ContractAddresses.feeAssetHandlerAddress?.toString()}`); } } diff --git a/yarn-project/cli/src/cmds/l1/index.ts b/yarn-project/cli/src/cmds/l1/index.ts index 0e291874633d..6d7fb2073e05 100644 --- a/yarn-project/cli/src/cmds/l1/index.ts +++ b/yarn-project/cli/src/cmds/l1/index.ts @@ -457,6 +457,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger .addOption(l1ChainIdOption) .requiredOption('-t, --token ', 'The address of the token to bridge', parseEthereumAddress) .requiredOption('-p, --portal ', 'The address of the portal contract', parseEthereumAddress) + .option('-f, --faucet ', 'The address of the faucet contract (only used if minting)', parseEthereumAddress) .option('--l1-private-key ', 'The private key to use for deployment', PRIVATE_KEY) .option('--json', 'Output the claim in JSON format') .action(async (amount, recipient, options) => { @@ -469,6 +470,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: Logger options.l1PrivateKey, options.mnemonic, options.token, + options.faucet, options.portal, options.private, options.mint, diff --git a/yarn-project/cli/src/cmds/pxe/get_node_info.ts b/yarn-project/cli/src/cmds/pxe/get_node_info.ts index b9e9179d2d5b..991e2b07dd4c 100644 --- a/yarn-project/cli/src/cmds/pxe/get_node_info.ts +++ b/yarn-project/cli/src/cmds/pxe/get_node_info.ts @@ -35,6 +35,7 @@ export async function getNodeInfo( governanceProposer: info.l1ContractAddresses.governanceProposerAddress.toString(), governance: info.l1ContractAddresses.governanceAddress.toString(), slashFactory: info.l1ContractAddresses.slashFactoryAddress?.toString(), + feeAssetHandler: info.l1ContractAddresses.feeAssetHandlerAddress?.toString(), }, protocolContractAddresses: { classRegisterer: info.protocolContractAddresses.classRegisterer.toString(), @@ -61,7 +62,7 @@ export async function getNodeInfo( log(` GovernanceProposer Address: ${info.l1ContractAddresses.governanceProposerAddress.toString()}`); log(` Governance Address: ${info.l1ContractAddresses.governanceAddress.toString()}`); log(` SlashFactory Address: ${info.l1ContractAddresses.slashFactoryAddress?.toString()}`); - + log(` FeeAssetHandler Address: ${info.l1ContractAddresses.feeAssetHandlerAddress?.toString()}`); log(`L2 Contract Addresses:`); log(` Class Registerer: ${info.protocolContractAddresses.classRegisterer.toString()}`); log(` Fee Juice: ${info.protocolContractAddresses.feeJuice.toString()}`); diff --git a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts index 61f0e9c467fa..99db18968f05 100644 --- a/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts +++ b/yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts @@ -119,15 +119,6 @@ export class CrossChainMessagingTest { ['Underlying', 'UND', walletClient.account.address], ).then(({ address }) => address); - const underlyingERC20 = getContract({ - address: underlyingERC20Address!.toString(), - abi: TestERC20Abi, - client: walletClient, - }); - - // allow anyone to mint - await underlyingERC20.write.setFreeForAll([true], {} as any); - this.logger.verbose(`Setting up cross chain harness...`); this.crossChainTestHarness = await CrossChainTestHarness.new( this.aztecNode, diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index db8b1daa631b..da2d1c2023e0 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -164,8 +164,9 @@ export class FeesTest { await this.snapshotManager.snapshot( 'initial_accounts', deployAccounts(this.numberOfAccounts, this.logger), - async ({ deployedAccounts }, { pxe, aztecNode, aztecNodeConfig }) => { + async ({ deployedAccounts }, { pxe, aztecNode, aztecNodeConfig, deployL1ContractsValues }) => { this.pxe = pxe; + this.aztecNode = aztecNode; this.gasSettings = GasSettings.default({ maxFeesPerGas: (await this.aztecNode.getCurrentBaseFees()).mul(2) }); this.cheatCodes = await CheatCodes.create(aztecNodeConfig.l1RpcUrls, pxe); @@ -181,13 +182,12 @@ export class FeesTest { this.feeJuiceContract = await FeeJuiceContract.at(canonicalFeeJuice.address, this.aliceWallet); this.coinbase = EthAddress.random(); - const { publicClient, walletClient } = createL1Clients(aztecNodeConfig.l1RpcUrls, MNEMONIC); this.feeJuiceBridgeTestHarness = await FeeJuicePortalTestingHarnessFactory.create({ aztecNode, aztecNodeAdmin: aztecNode, pxeService: pxe, - publicClient, - walletClient, + publicClient: deployL1ContractsValues.publicClient, + walletClient: deployL1ContractsValues.walletClient, wallet: this.aliceWallet, logger: this.logger, }); @@ -214,13 +214,12 @@ export class FeesTest { this.getGasBalanceFn = getBalancesFn('⛽', this.feeJuiceContract.methods.balance_of_public, this.logger); - const { publicClient, walletClient } = createL1Clients(context.aztecNodeConfig.l1RpcUrls, MNEMONIC); this.feeJuiceBridgeTestHarness = await FeeJuicePortalTestingHarnessFactory.create({ aztecNode: context.aztecNode, aztecNodeAdmin: context.aztecNode, pxeService: context.pxe, - publicClient: publicClient, - walletClient: walletClient, + publicClient: context.deployL1ContractsValues.publicClient, + walletClient: context.deployL1ContractsValues.walletClient, wallet: this.aliceWallet, logger: this.logger, }); diff --git a/yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts b/yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts index b34f0143d92f..f169fce9e1f9 100644 --- a/yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_bridge_tutorial_test.test.ts @@ -11,7 +11,14 @@ import { waitForPXE, } from '@aztec/aztec.js'; import { createL1Clients, deployL1Contract } from '@aztec/ethereum'; -import { TestERC20Abi, TestERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } from '@aztec/l1-artifacts'; +import { + FeeAssetHandlerAbi, + FeeAssetHandlerBytecode, + TestERC20Abi, + TestERC20Bytecode, + TokenPortalAbi, + TokenPortalBytecode, +} from '@aztec/l1-artifacts'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { TokenBridgeContract } from '@aztec/noir-contracts.js/TokenBridge'; @@ -25,6 +32,8 @@ const { ETHEREUM_HOSTS = 'http://localhost:8545' } = process.env; const { walletClient, publicClient } = createL1Clients(ETHEREUM_HOSTS.split(','), MNEMONIC); const ownerEthAddress = walletClient.account.address; +const MINT_AMOUNT = BigInt(1e15); + const setupSandbox = async () => { const { PXE_URL = 'http://localhost:8080' } = process.env; // eslint-disable-next-line @typescript-eslint/await-thenable @@ -41,18 +50,37 @@ async function deployTestERC20(): Promise { ); } +async function deployFeeAssetHandler(l1TokenContract: EthAddress): Promise { + const constructorArgs = [walletClient.account.address, l1TokenContract.toString(), MINT_AMOUNT]; + return await deployL1Contract( + walletClient, + publicClient, + FeeAssetHandlerAbi, + FeeAssetHandlerBytecode, + constructorArgs, + ).then(({ address }) => address); +} + async function deployTokenPortal(): Promise { return await deployL1Contract(walletClient, publicClient, TokenPortalAbi, TokenPortalBytecode, []).then( ({ address }) => address, ); } + +async function addMinter(l1TokenContract: EthAddress, l1TokenHandler: EthAddress) { + const contract = getContract({ + address: l1TokenContract.toString(), + abi: TestERC20Abi, + client: walletClient, + }); + await contract.write.addMinter([l1TokenHandler.toString()]); +} // docs:end:utils describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { it('Deploys tokens & bridges to L1 & L2, mints & publicly bridges tokens', async () => { // docs:start:setup const logger = createLogger('aztec:token-bridge-tutorial'); - const amount = BigInt(100); const pxe = await setupSandbox(); const wallets = await getInitialTestAccountsWallets(pxe); const ownerWallet = wallets[0]; @@ -78,7 +106,10 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { const l1TokenContract = await deployTestERC20(); logger.info('erc20 contract deployed'); - const l1TokenManager = new L1TokenManager(l1TokenContract, publicClient, walletClient, logger); + const feeAssetHandler = await deployFeeAssetHandler(l1TokenContract); + await addMinter(l1TokenContract, feeAssetHandler); + + const l1TokenManager = new L1TokenManager(l1TokenContract, feeAssetHandler, publicClient, walletClient, logger); // docs:end:deploy-l1-token // Deploy L1 portal contract @@ -120,6 +151,7 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { const l1PortalManager = new L1TokenPortalManager( l1PortalContractAddress, l1TokenContract, + feeAssetHandler, l1ContractAddresses.outboxAddress, publicClient, walletClient, @@ -128,7 +160,7 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { // docs:end:setup-portal // docs:start:l1-bridge-public - const claim = await l1PortalManager.bridgeTokensPublic(ownerAztecAddress, amount, true); + const claim = await l1PortalManager.bridgeTokensPublic(ownerAztecAddress, MINT_AMOUNT, true); // Do 2 unrleated actions because // https://github.com/AztecProtocol/aztec-packages/blob/7e9e2681e314145237f95f79ffdc95ad25a0e319/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts#L354-L355 @@ -139,7 +171,7 @@ describe('e2e_cross_chain_messaging token_bridge_tutorial_test', () => { // Claim tokens publicly on L2 // docs:start:claim await l2BridgeContract.methods - .claim_public(ownerAztecAddress, amount, claim.claimSecret, claim.messageLeafIndex) + .claim_public(ownerAztecAddress, MINT_AMOUNT, claim.claimSecret, claim.messageLeafIndex) .send() .wait(); const balance = await l2TokenContract.methods.balance_of_public(ownerAztecAddress).simulate(); diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index afe4f8a51c8b..3df3c05b010c 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -205,6 +205,7 @@ export class CrossChainTestHarness { this.l1TokenPortalManager = new L1TokenPortalManager( this.tokenPortalAddress, this.underlyingERC20Address, + this.l1ContractAddresses.feeAssetHandlerAddress, this.l1ContractAddresses.outboxAddress, this.publicClient, this.walletClient, @@ -215,7 +216,12 @@ export class CrossChainTestHarness { } async mintTokensOnL1(amount: bigint) { - await this.l1TokenManager.mint(amount, this.ethAccount.toString()); + const contract = getContract({ + abi: TestERC20Abi, + address: this.l1TokenManager.tokenAddress.toString(), + client: this.walletClient, + }); + await contract.write.mint([this.ethAccount.toString(), amount]); expect(await this.l1TokenManager.getL1TokenBalance(this.ethAccount.toString())).toEqual(amount); } diff --git a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts index 34efd5944df8..1699baaf61ab 100644 --- a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts @@ -12,10 +12,13 @@ import { retryUntil, } from '@aztec/aztec.js'; import type { ViemPublicClient, ViemWalletClient } from '@aztec/ethereum'; +import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi'; import { FeeJuiceContract } from '@aztec/noir-contracts.js/FeeJuice'; import { ProtocolContractAddress } from '@aztec/protocol-contracts'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; +import { getContract } from 'viem'; + export interface IGasBridgingTestHarness { getL1FeeJuiceBalance(address: EthAddress): Promise; prepareTokensOnL1(bridgeAmount: bigint, owner: AztecAddress): Promise; @@ -62,6 +65,7 @@ export class FeeJuicePortalTestingHarnessFactory { ethAccount, feeJuicePortalAddress, feeJuiceAddress, + l1ContractAddresses.feeAssetHandlerAddress!, publicClient, walletClient, ); @@ -101,6 +105,8 @@ export class GasBridgingTestHarness implements IGasBridgingTestHarness { public feeJuicePortalAddress: EthAddress, /** Underlying token for portal tests. */ public l1FeeJuiceAddress: EthAddress, + /** Fee asset handler address. */ + public feeAssetHandlerAddress: EthAddress, /** Viem Public client instance. */ public publicClient: ViemPublicClient, /** Viem Wallet Client instance. */ @@ -109,6 +115,7 @@ export class GasBridgingTestHarness implements IGasBridgingTestHarness { this.feeJuicePortalManager = new L1FeeJuicePortalManager( this.feeJuicePortalAddress, this.l1FeeJuiceAddress, + this.feeAssetHandlerAddress, this.publicClient, this.walletClient, this.logger, @@ -118,17 +125,25 @@ export class GasBridgingTestHarness implements IGasBridgingTestHarness { } async mintTokensOnL1(amount: bigint, to: EthAddress = this.ethAccount) { - const balanceBefore = await this.l1TokenManager.getL1TokenBalance(to.toString()); - await this.l1TokenManager.mint(amount, to.toString()); - expect(await this.l1TokenManager.getL1TokenBalance(to.toString())).toEqual(balanceBefore + amount); + // const balanceBefore = await this.l1TokenManager.getL1TokenBalance(to.toString()); + await this.l1TokenManager.mint(to.toString()); + const feeAssetL1 = getContract({ + address: this.l1FeeJuiceAddress.toString(), + abi: TestERC20Abi, + client: this.walletClient, + }); + + await feeAssetL1.write.mint([to.toString(), amount]); + + // expect(await this.l1TokenManager.getL1TokenBalance(to.toString())).toEqual(balanceBefore + amount); } async getL1FeeJuiceBalance(address: EthAddress) { return await this.l1TokenManager.getL1TokenBalance(address.toString()); } - sendTokensToPortalPublic(bridgeAmount: bigint, l2Address: AztecAddress, mint = false) { - return this.feeJuicePortalManager.bridgeTokensPublic(l2Address, bridgeAmount, mint); + sendTokensToPortalPublic(bridgeAmount: bigint, l2Address: AztecAddress) { + return this.feeJuicePortalManager.bridgeTokensPublic(l2Address, bridgeAmount, false); } async consumeMessageOnAztecAndClaimPrivately(owner: AztecAddress, claim: L2AmountClaim) { @@ -147,7 +162,8 @@ export class GasBridgingTestHarness implements IGasBridgingTestHarness { } async prepareTokensOnL1(bridgeAmount: bigint, owner: AztecAddress) { - const claim = await this.sendTokensToPortalPublic(bridgeAmount, owner, true); + await this.mintTokensOnL1(bridgeAmount); + const claim = await this.sendTokensToPortalPublic(bridgeAmount, owner); const isSynced = async () => await this.aztecNode.isL1ToL2MessageSynced(Fr.fromHexString(claim.messageHash)); await retryUntil(isSynced, `message ${claim.messageHash} sync`, 24, 1); @@ -165,7 +181,6 @@ export class GasBridgingTestHarness implements IGasBridgingTestHarness { // Consume L1 -> L2 message and claim tokens privately on L2 await this.consumeMessageOnAztecAndClaimPrivately(owner, claim); - await this.expectPublicBalanceOnL2(owner, bridgeAmount); } private async advanceL2Block() { diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index f3935e08223e..2666d0321c5a 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -56,7 +56,6 @@ export async function deployTestWalletWithTokens( mintAmount: bigint, logger: Logger, numberOfFundedWallets = 1, - initialFeeJuice = 10n ** 22n, ): Promise { const pxe = await createCompatibleClient(pxeUrl, logger); const node = createAztecNodeClient(nodeUrl); @@ -71,9 +70,7 @@ export async function deployTestWalletWithTokens( const fundedAccounts = await Promise.all(funded.map(a => getSchnorrAccount(pxe, a.secret, a.signingKey, a.salt))); const claims = await Promise.all( - fundedAccounts.map(a => - bridgeL1FeeJuice(l1RpcUrls, mnemonicOrPrivateKey, pxe, a.getAddress(), initialFeeJuice, logger), - ), + fundedAccounts.map(a => bridgeL1FeeJuice(l1RpcUrls, mnemonicOrPrivateKey, pxe, a.getAddress(), undefined, logger)), ); // Progress by 2 L2 blocks so that the l1ToL2Message added above will be available to use on L2. @@ -102,7 +99,7 @@ async function bridgeL1FeeJuice( mnemonicOrPrivateKey: string, pxe: PXE, recipient: AztecAddress, - amount: bigint, + amount: bigint | undefined, log: Logger, ) { const { l1ChainId } = await pxe.getNodeInfo(); diff --git a/yarn-project/ethereum/src/contracts/fee_asset_handler.test.ts b/yarn-project/ethereum/src/contracts/fee_asset_handler.test.ts new file mode 100644 index 000000000000..de006a7ffa67 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/fee_asset_handler.test.ts @@ -0,0 +1,76 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; +import { Fr } from '@aztec/foundation/fields'; +import { type Logger, createLogger } from '@aztec/foundation/log'; +import { TestERC20Abi as FeeAssetAbi } from '@aztec/l1-artifacts/TestERC20Abi'; + +import type { Anvil } from '@viem/anvil'; +import omit from 'lodash.omit'; +import { type GetContractReturnType, getContract } from 'viem'; +import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; + +import { DefaultL1ContractsConfig } from '../config.js'; +import { createL1Clients, deployL1Contracts } from '../deploy_l1_contracts.js'; +import { L1TxUtils } from '../l1_tx_utils.js'; +import { startAnvil } from '../test/start_anvil.js'; +import type { L1Clients } from '../types.js'; +import { FeeAssetHandlerContract } from './fee_asset_handler.js'; + +const originalVersionSalt = 42; + +describe('FeeAssetHandler', () => { + let anvil: Anvil; + let rpcUrl: string; + let privateKey: PrivateKeyAccount; + let logger: Logger; + + let feeAssetHandler: FeeAssetHandlerContract; + let feeAsset: GetContractReturnType; + + beforeAll(async () => { + logger = createLogger('ethereum:test:fee_asset_handler'); + // this is the 6th address that gets funded by the junk mnemonic + privateKey = privateKeyToAccount('0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba'); + const vkTreeRoot = Fr.random(); + const protocolContractTreeRoot = Fr.random(); + const l2FeeJuiceAddress = Fr.random(); + + ({ anvil, rpcUrl } = await startAnvil()); + + const { publicClient, walletClient } = createL1Clients([rpcUrl], privateKey); + + const deployed = await deployL1Contracts([rpcUrl], privateKey, foundry, logger, { + ...DefaultL1ContractsConfig, + salt: originalVersionSalt, + vkTreeRoot, + protocolContractTreeRoot, + l2FeeJuiceAddress, + genesisArchiveRoot: Fr.random(), + genesisBlockHash: Fr.random(), + }); + // Since the registry cannot "see" the slash factory, we omit it from the addresses for this test + const deployedAddresses = omit(deployed.l1ContractAddresses, 'slashFactoryAddress'); + const txUtils = new L1TxUtils(publicClient, walletClient, logger); + feeAssetHandler = new FeeAssetHandlerContract(deployedAddresses.feeAssetHandlerAddress!.toString(), txUtils); + feeAsset = getContract({ + address: deployedAddresses.feeJuiceAddress!.toString(), + abi: FeeAssetAbi, + client: publicClient, + }); + }); + + afterAll(async () => { + await anvil.stop(); + }); + + it('should mint fee asset', async () => { + const address = EthAddress.random(); + for (let i = 1; i <= 10; i++) { + const txHash = await feeAssetHandler.mint(address.toString()); + expect(txHash.receipt.status).toBe('success'); + logger.verbose(`Minted fee asset in ${txHash}`); + const balance = await feeAsset.read.balanceOf([address.toString()]); + expect(balance).toBe((await feeAssetHandler.getMintAmount()) * BigInt(i)); + } + }); +}); diff --git a/yarn-project/ethereum/src/contracts/fee_asset_handler.ts b/yarn-project/ethereum/src/contracts/fee_asset_handler.ts new file mode 100644 index 000000000000..12e7e1b4cbe1 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/fee_asset_handler.ts @@ -0,0 +1,45 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; +import { FeeAssetHandlerAbi } from '@aztec/l1-artifacts/FeeAssetHandlerAbi'; + +import { type Hex, encodeFunctionData, getContract } from 'viem'; + +import type { L1TxUtils } from '../l1_tx_utils.js'; + +export class FeeAssetHandlerContract { + public address: EthAddress; + + constructor(address: Hex, public readonly txUtils: L1TxUtils) { + this.address = EthAddress.fromString(address); + } + + public getMintAmount() { + const contract = getContract({ + abi: FeeAssetHandlerAbi, + address: this.address.toString(), + client: this.txUtils.publicClient, + }); + return contract.read.mintAmount(); + } + + public mint(recipient: Hex) { + return this.txUtils.sendAndMonitorTransaction({ + to: this.address.toString(), + data: encodeFunctionData({ + abi: FeeAssetHandlerAbi, + functionName: 'mint', + args: [recipient], + }), + }); + } + + public setMintAmount(amount: bigint) { + return this.txUtils.sendAndMonitorTransaction({ + to: this.address.toString(), + data: encodeFunctionData({ + abi: FeeAssetHandlerAbi, + functionName: 'setMintAmount', + args: [amount], + }), + }); + } +} diff --git a/yarn-project/ethereum/src/contracts/forwarder.test.ts b/yarn-project/ethereum/src/contracts/forwarder.test.ts index 8ab31e63b923..3d2339025775 100644 --- a/yarn-project/ethereum/src/contracts/forwarder.test.ts +++ b/yarn-project/ethereum/src/contracts/forwarder.test.ts @@ -88,8 +88,8 @@ describe('Forwarder', () => { client: publicClient, }); - const freeForAllHash = await tokenContract.write.setFreeForAll([true], { account: privateKey }); - await publicClient.waitForTransactionReceipt({ hash: freeForAllHash }); + const addMinterHash = await tokenContract.write.addMinter([forwarder.getAddress()], { account: privateKey }); + await publicClient.waitForTransactionReceipt({ hash: addMinterHash }); logger.info(`Token address: ${tokenAddress}`); }); diff --git a/yarn-project/ethereum/src/contracts/index.ts b/yarn-project/ethereum/src/contracts/index.ts index 0db47645c7cd..3b7cc24a6b68 100644 --- a/yarn-project/ethereum/src/contracts/index.ts +++ b/yarn-project/ethereum/src/contracts/index.ts @@ -1,4 +1,5 @@ export * from './empire_base.js'; +export * from './fee_asset_handler.js'; export * from './fee_juice.js'; export * from './forwarder.js'; export * from './governance.js'; diff --git a/yarn-project/ethereum/src/contracts/registry.test.ts b/yarn-project/ethereum/src/contracts/registry.test.ts index dfc3b22f4781..38604c35be04 100644 --- a/yarn-project/ethereum/src/contracts/registry.test.ts +++ b/yarn-project/ethereum/src/contracts/registry.test.ts @@ -53,7 +53,7 @@ describe('Registry', () => { genesisBlockHash: Fr.random(), }); // Since the registry cannot "see" the slash factory, we omit it from the addresses for this test - deployedAddresses = omit(deployed.l1ContractAddresses, 'slashFactoryAddress'); + deployedAddresses = omit(deployed.l1ContractAddresses, 'slashFactoryAddress', 'feeAssetHandlerAddress'); registry = new RegistryContract(publicClient, deployedAddresses.registryAddress); }); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 07fe66ab5162..64ac544b508c 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -6,6 +6,8 @@ import { CoinIssuerBytecode, ExtRollupLibAbi, ExtRollupLibBytecode, + FeeAssetHandlerAbi, + FeeAssetHandlerBytecode, FeeJuicePortalAbi, FeeJuicePortalBytecode, ForwarderAbi, @@ -188,6 +190,10 @@ export const l1Artifacts = { contractAbi: RegisterNewRollupVersionPayloadAbi, contractBytecode: RegisterNewRollupVersionPayloadBytecode as Hex, }, + feeAssetHandler: { + contractAbi: FeeAssetHandlerAbi, + contractBytecode: FeeAssetHandlerBytecode as Hex, + }, }; export interface DeployL1ContractsArgs extends L1ContractsConfig { @@ -458,6 +464,44 @@ export const cheat_initializeValidatorSet = async ( } }; +/** + * Initialize the fee asset handler and make it a minter on the fee asset. + * @note This function will only be used for testing purposes. + * + * @param clients - The L1 clients. + * @param deployer - The L1 deployer. + * @param feeAssetAddress - The address of the fee asset. + * @param logger - The logger. + */ +// eslint-disable-next-line camelcase +export const cheat_initializeFeeAssetHandler = async ( + clients: L1Clients, + deployer: L1Deployer, + feeAssetAddress: EthAddress, + logger: Logger, +): Promise<{ + feeAssetHandlerAddress: EthAddress; + txHash: Hex; +}> => { + const feeAssetHandlerAddress = await deployer.deploy(l1Artifacts.feeAssetHandler, [ + clients.walletClient.account.address, + feeAssetAddress.toString(), + BigInt(1e18), + ]); + logger.verbose(`Deployed FeeAssetHandler at ${feeAssetHandlerAddress}`); + + const { txHash } = await deployer.sendTransaction({ + to: feeAssetAddress.toString(), + data: encodeFunctionData({ + abi: l1Artifacts.feeAsset.contractAbi, + functionName: 'addMinter', + args: [feeAssetHandlerAddress.toString()], + }), + }); + logger.verbose(`Added fee asset handler ${feeAssetHandlerAddress} as minter on fee asset in ${txHash}`); + return { feeAssetHandlerAddress, txHash }; +}; + /** * Deploys the aztec L1 contracts; Rollup & (optionally) Decoder Helper. * @param rpcUrls - List of URLs of the ETH RPC to use for deployment. @@ -577,29 +621,24 @@ export const deployL1Contracts = async ( // Transaction hashes to await const txHashes: Hex[] = []; - if (args.acceleratedTestDeployments || !(await feeAsset.read.freeForAll())) { - const { txHash } = await deployer.sendTransaction({ - to: feeAssetAddress.toString(), - data: encodeFunctionData({ - abi: l1Artifacts.feeAsset.contractAbi, - functionName: 'setFreeForAll', - args: [true], - }), - }); - logger.verbose(`Fee asset set to free for all in ${txHash}`); - txHashes.push(txHash); - } + const { feeAssetHandlerAddress, txHash: feeAssetHandlerTxHash } = await cheat_initializeFeeAssetHandler( + clients, + deployer, + feeAssetAddress, + logger, + ); + txHashes.push(feeAssetHandlerTxHash); - if (args.acceleratedTestDeployments || (await feeAsset.read.owner()) !== getAddress(coinIssuerAddress.toString())) { + if (args.acceleratedTestDeployments || !(await feeAsset.read.minters([coinIssuerAddress.toString()]))) { const { txHash } = await deployer.sendTransaction({ to: feeAssetAddress.toString(), data: encodeFunctionData({ abi: l1Artifacts.feeAsset.contractAbi, - functionName: 'transferOwnership', + functionName: 'addMinter', args: [coinIssuerAddress.toString()], }), }); - logger.verbose(`Fee asset transferred ownership to coin issuer in ${txHash}`); + logger.verbose(`Added coin issuer ${coinIssuerAddress} as minter on fee asset in ${txHash}`); txHashes.push(txHash); } @@ -752,6 +791,7 @@ export const deployL1Contracts = async ( l1ContractAddresses: { ...l1Contracts, slashFactoryAddress, + feeAssetHandlerAddress, }, }; }; diff --git a/yarn-project/ethereum/src/l1_contract_addresses.ts b/yarn-project/ethereum/src/l1_contract_addresses.ts index a4dd1da75389..4b625ca5ebc4 100644 --- a/yarn-project/ethereum/src/l1_contract_addresses.ts +++ b/yarn-project/ethereum/src/l1_contract_addresses.ts @@ -26,7 +26,7 @@ export const L1ContractsNames = [ /** Provides the directory of current L1 contract addresses */ export type L1ContractAddresses = { [K in (typeof L1ContractsNames)[number]]: EthAddress; -} & { slashFactoryAddress?: EthAddress | undefined }; +} & { slashFactoryAddress?: EthAddress | undefined; feeAssetHandlerAddress?: EthAddress | undefined }; export const L1ContractAddressesSchema = z.object({ rollupAddress: schemas.EthAddress, @@ -41,6 +41,7 @@ export const L1ContractAddressesSchema = z.object({ governanceProposerAddress: schemas.EthAddress, governanceAddress: schemas.EthAddress, slashFactoryAddress: schemas.EthAddress.optional(), + feeAssetHandlerAddress: schemas.EthAddress.optional(), }) satisfies ZodFor; const parseEnv = (val: string) => EthAddress.fromString(val); @@ -106,4 +107,9 @@ export const l1ContractAddressesMapping: ConfigMappingsType description: 'The deployed L1 slashFactory contract address', parseEnv, }, + feeAssetHandlerAddress: { + env: 'FEE_ASSET_HANDLER_CONTRACT_ADDRESS', + description: 'The deployed L1 feeAssetHandler contract address', + parseEnv, + }, }; diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 28e61a0c87a8..937dc6d33a3d 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -211,4 +211,5 @@ export type EnvVar = | 'K8S_POD_UID' | 'K8S_NAMESPACE_NAME' | 'CUSTOM_FORWARDER_CONTRACT_ADDRESS' - | 'P2P_TRUSTED_PEERS'; + | 'P2P_TRUSTED_PEERS' + | 'FEE_ASSET_HANDLER_CONTRACT_ADDRESS'; diff --git a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh index 2a4cf1b092a4..17c85ae26f31 100755 --- a/yarn-project/l1-artifacts/scripts/generate-artifacts.sh +++ b/yarn-project/l1-artifacts/scripts/generate-artifacts.sh @@ -11,31 +11,32 @@ cd $(git rev-parse --show-toplevel)/yarn-project/l1-artifacts # - {name}Bytecode.ts: contains the bytecode and link references contracts=( - "Registry" - "Inbox" - "Outbox" - "Rollup" - "TokenPortal" - "TestERC20" - "UniswapPortal" - "IERC20" - "FeeJuicePortal" - "MockVerifier" - "IVerifier" "CoinIssuer" - "RewardDistributor" - "GovernanceProposer" + "EmpireBase" + "ExtRollupLib" + "FeeJuicePortal" + "Forwarder" "Governance" + "GovernanceProposer" + "FeeAssetHandler" + "HonkVerifier" + "IERC20" + "Inbox" + "IVerifier" + "MockVerifier" "NewGovernanceProposerPayload" + "Outbox" "RegisterNewRollupVersionPayload" - "ValidatorSelectionLib" - "ExtRollupLib" - "SlashingProposer" + "Registry" + "RewardDistributor" + "Rollup" "Slasher" - "EmpireBase" "SlashFactory" - "Forwarder" - "HonkVerifier" + "SlashingProposer" + "TestERC20" + "TokenPortal" + "UniswapPortal" + "ValidatorSelectionLib" ) # Combine error ABIs once, removing duplicates by {type, name}.