Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/deploy-fisherman-network.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ jobs:
echo "USE_NETWORK_CONFIG=true" >> $GITHUB_ENV
elif [[ "${{ inputs.l1_network }}" == "mainnet" ]]; then
echo "NETWORK=mainnet" >> $GITHUB_ENV
echo "NAMESPACE=ignition-fisherman-mainnet" >> $GITHUB_ENV
echo "NAMESPACE=mainnet" >> $GITHUB_ENV
echo "ETHEREUM_CHAIN_ID=1" >> $GITHUB_ENV
echo "L1_NETWORK=mainnet" >> $GITHUB_ENV
echo "SNAPSHOT_BUCKET_DIRECTORY=ignition-mainnet" >> $GITHUB_ENV
echo "SNAPSHOT_BUCKET_DIRECTORY=mainnet" >> $GITHUB_ENV
echo "USE_NETWORK_CONFIG=true" >> $GITHUB_ENV
fi

Expand Down
5 changes: 5 additions & 0 deletions .test_patterns.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ tests:
error_regex: "ultra_circuit_builder.test.cpp:631: Failure"
owners:
- *luke
# http://ci.aztec-labs.com/1593f7c89e22b51b
- regex: stdlib_primitives_tests stdlibBiggroupSecp256k1/1.WnafSecp256k1StaggerOutOfRangeFails
error_regex: "biggroup_nafs: stagger fragment is not in range"
owners:
- *luke

# noir
# Something to do with how I run the tests now. Think these are fine in nextest.
Expand Down
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"merkleizing",
"messagebox",
"mimc",
"mintable",
"mktemp",
"mload",
"mockify",
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/lib/circuits
Submodule circuits updated 410 files
2 changes: 1 addition & 1 deletion l1-contracts/script/StakingAssetHandler.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ contract StakingAssetHandlerScript is Test {
bytes32 public constant DEPOSIT_MERKLE_ROOT = bytes32(0);

ZKPassportVerifier internal constant zkPassportVerifier =
ZKPassportVerifier(0xf7480fd0A9289c062C52532f11D31e0b7A30ABe3);
ZKPassportVerifier(0x3101Bad9eA5fACadA5554844a1a88F7Fe48D4DE0);

TestERC20 public constant stakingAsset = TestERC20(0x6732CEDafCBF85Afa9B5C83f0385967840BBCe47);
IRegistry public constant registry = IRegistry(0xc2F24280F5c7F4897370dFDEb30f79Ded14f1c81);
Expand Down
1 change: 0 additions & 1 deletion l1-contracts/src/core/interfaces/IRollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ struct RollupConfig {
uint32 version;
IERC20 feeAsset;
IFeeJuicePortal feeAssetPortal;
IRewardDistributor rewardDistributor;
IVerifier epochProofVerifier;
IInbox inbox;
IOutbox outbox;
Expand Down
7 changes: 4 additions & 3 deletions l1-contracts/src/core/libraries/rollup/BlobLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {Vm} from "forge-std/Vm.sol";
* The VM_ADDRESS (0x7109709ECfa91a80626fF3989D68f67F5b1DD12D) is a special address used to detect
* when the contract is running in a Foundry test environment. This address is derived from
* keccak256("hevm cheat code") and corresponds to Foundry's VM contract that provides testing utilities.
* When VM_ADDRESS.code.length > 0, it indicates we're in a test environment, allowing the library to:
* When block.chainid == 31337 && VM_ADDRESS.code.length > 0, it indicates we're in a test environment,
* allowing the library to:
* - Use Foundry's getBlobBaseFee() cheatcode instead of block.blobbasefee
* - Use Foundry's getBlobhashes() cheatcode instead of the blobhash() opcode
* This enables comprehensive testing of blob functionality without requiring actual blob transactions.
Expand All @@ -47,7 +48,7 @@ library BlobLib {
* @return uint256 - The blob base fee
*/
function getBlobBaseFee() internal view returns (uint256) {
if (VM_ADDRESS.code.length > 0) {
if (block.chainid == 31_337 && VM_ADDRESS.code.length > 0) {
return Vm(VM_ADDRESS).getBlobBaseFee();
}
return block.blobbasefee;
Expand All @@ -62,7 +63,7 @@ library BlobLib {
* @return blobHash - The blob hash
*/
function getBlobHash(uint256 _index) internal view returns (bytes32 blobHash) {
if (VM_ADDRESS.code.length > 0) {
if (block.chainid == 31_337 && VM_ADDRESS.code.length > 0) {
// We know that this one is ABHORRENT. But it should not exists, and only will
// be hit in testing.
bytes32[] memory blobHashes = Vm(VM_ADDRESS).getBlobhashes();
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/src/core/messagebridge/Inbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ contract Inbox is IInbox {

bytes16 updatedRollingHash = bytes16(keccak256(abi.encodePacked(rollingHash, leaf)));
state = InboxState({
rollingHash: bytes16(updatedRollingHash),
rollingHash: updatedRollingHash,
totalMessagesInserted: totalMessagesInserted + 1,
inProgress: inProgress
});
Expand Down
2 changes: 1 addition & 1 deletion l1-contracts/src/core/reward-boost/RewardBooster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ contract RewardBooster is IBooster {
}

function _toShares(uint256 _value) internal view returns (uint256) {
if (_value > CONFIG_MAX_SCORE) {
if (_value >= CONFIG_MAX_SCORE) {
return CONFIG_K;
}
uint256 t = (CONFIG_MAX_SCORE - _value);
Expand Down
104 changes: 90 additions & 14 deletions l1-contracts/src/governance/CoinIssuer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,120 @@ import {Ownable2Step} from "@oz/access/Ownable2Step.sol";
/**
* @title CoinIssuer
* @author Aztec Labs
* @notice A contract that allows minting of coins at a maximum fixed rate
* @notice A contract that allows minting of coins at a maximum percentage rate per year using discrete annual budgets
*
* This contract uses a discrete annual budget model:
* - Years are fixed periods from deployment:
* - year 0 = [deployment, deployment + 365d)
* - year 1 = [deployment + 365d, deployment + (2) * 365d)
* - ...
* - year n = [deployment + 365d * n, deployment + (n + 1) * 365d)
* - Each year's budget is calculated at the start of that year based on the actual supply at that moment
* - Budget = totalSupply() × NOMINAL_ANNUAL_PERCENTAGE_CAP / 1e18
* - Unused budget from year N is LOST when year N+1 begins (use-it-or-lose-it)
*
* Rate semantics: If the full budget is minted every year, the effective annual inflation rate equals
* NOMINAL_ANNUAL_PERCENTAGE_CAP. For example, setting the rate to 0.10e18 (10%) and fully minting each
* year will result in supply growing by exactly 10% annually: supply(year N) = supply(year 0) × (1.10)^N
*
* Partial minting: If less than the full budget is minted in year N, the remaining allowance is lost
* at the year N→N+1 boundary. Year N+1's budget is calculated based on the actual supply at the start
* of year N+1, which reflects only what was actually minted.
*
* @dev The NOMINAL_ANNUAL_PERCENTAGE_CAP is in e18 precision where 1e18 = 100%
*
* @dev The token MUST have a non-zero initial supply at deployment, or an alternative way to mint the token.
*/
contract CoinIssuer is ICoinIssuer, Ownable {
IMintableERC20 public immutable ASSET;
uint256 public immutable RATE;
uint256 public timeOfLastMint;
uint256 public immutable NOMINAL_ANNUAL_PERCENTAGE_CAP;
uint256 public immutable DEPLOYMENT_TIME;

constructor(IMintableERC20 _asset, uint256 _rate, address _owner) Ownable(_owner) {
// Note that the state variables below are "cached":
// they are only updated when minting after a year boundary.
uint256 public cachedBudgetYear;
uint256 public cachedBudget;

constructor(IMintableERC20 _asset, uint256 _annualPercentage, address _owner) Ownable(_owner) {
ASSET = _asset;
RATE = _rate;
timeOfLastMint = block.timestamp;
NOMINAL_ANNUAL_PERCENTAGE_CAP = _annualPercentage;
DEPLOYMENT_TIME = block.timestamp;

cachedBudgetYear = 0;
cachedBudget = _getNewBudget();

emit BudgetReset(0, cachedBudget);
}

function acceptTokenOwnership() external override(ICoinIssuer) onlyOwner {
Ownable2Step(address(ASSET)).acceptOwnership();
}

/**
* @notice Mint tokens up to the `mintAvailable` limit
* Beware that the mintAvailable will be reset to 0, and not just
* reduced by the amount minted.
* @notice Mint `_amount` tokens to `_to`
*
* @dev The `_amount` must be within the `cachedBudget`
*
* @param _to - The address to receive the funds
* @param _amount - The amount to mint
*/
function mint(address _to, uint256 _amount) external override(ICoinIssuer) onlyOwner {
uint256 maxMint = mintAvailable();
require(_amount <= maxMint, Errors.CoinIssuer__InsufficientMintAvailable(maxMint, _amount));
timeOfLastMint = block.timestamp;
// Update state if we've crossed into a new year (will reset budget and forfeit unused amount)
_updateBudgetIfNeeded();

require(_amount <= cachedBudget, Errors.CoinIssuer__InsufficientMintAvailable(cachedBudget, _amount));
cachedBudget -= _amount;

ASSET.mint(_to, _amount);
}

/**
* @notice The amount of funds that is available for "minting"
* @notice The amount of funds that is available for "minting" in the current year
* If we've crossed into a new year since the last mint, returns the fresh budget
* for the new year based on current supply.
*
* @return The amount mintable
*/
function mintAvailable() public view override(ICoinIssuer) returns (uint256) {
return RATE * (block.timestamp - timeOfLastMint);
uint256 currentYear = _yearSinceGenesis();

// Until the budget is stale, return the cached budget
if (cachedBudgetYear >= currentYear) {
return cachedBudget;
}

// Crossed into new year(s): compute fresh budget
return _getNewBudget();
}

/**
* @notice Internal function to update year and budget when crossing year boundaries
*
* @dev If multiple years have passed without minting, jumps directly to current year
* and all intermediate years' budgets are lost
*/
function _updateBudgetIfNeeded() private {
uint256 currentYear = _yearSinceGenesis();
// If the budget is for the past, update the budget.
if (cachedBudgetYear < currentYear) {
cachedBudgetYear = currentYear;
cachedBudget = _getNewBudget();

emit BudgetReset(currentYear, cachedBudget);
}
}

/**
* @notice Internal function to compute the current year since genesis
*/
function _yearSinceGenesis() private view returns (uint256) {
return (block.timestamp - DEPLOYMENT_TIME) / 365 days;
}

/**
* @notice Internal function to compute a fresh budget
*/
function _getNewBudget() private view returns (uint256) {
return ASSET.totalSupply() * NOMINAL_ANNUAL_PERCENTAGE_CAP / 1e18;
}
}
2 changes: 2 additions & 0 deletions l1-contracts/src/governance/interfaces/ICoinIssuer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
pragma solidity >=0.8.27;

interface ICoinIssuer {
event BudgetReset(uint256 indexed newYear, uint256 newBudget);

function mint(address _to, uint256 _amount) external;
function acceptTokenOwnership() external;
function mintAvailable() external view returns (uint256);
Expand Down
48 changes: 28 additions & 20 deletions l1-contracts/src/mock/StakingAssetHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {IMintableERC20} from "@aztec/shared/interfaces/IMintableERC20.sol";
import {G1Point, G2Point} from "@aztec/shared/libraries/BN254Lib.sol";
import {Ownable} from "@oz/access/Ownable.sol";
import {MerkleProof} from "@oz/utils/cryptography/MerkleProof.sol";
import {ZKPassportVerifier, ProofVerificationParams, BoundData} from "@zkpassport/ZKPassportVerifier.sol";
import {
ZKPassportVerifier, ProofVerificationParams, BoundData, OS, FaceMatchMode
} from "@zkpassport/ZKPassportVerifier.sol";

/**
* @title StakingAssetHandler
Expand Down Expand Up @@ -56,6 +58,7 @@ interface IStakingAssetHandler {
error InvalidAge();
error InvalidCountry();
error InvalidValidityPeriod();
error InvalidFaceMatch();
error ExtraDiscloseDataNonZero();
error SybilDetected(bytes32 _nullifier);
error AttesterDoesNotExist(address _attester);
Expand Down Expand Up @@ -120,6 +123,12 @@ contract StakingAssetHandler is IStakingAssetHandler, Ownable {
string internal constant IRN = "IRN";
string internal constant CUB = "CUB";

// Minimum age
uint8 public constant MIN_AGE = 18;

// Validity period in seconds
uint256 public constant VALIDITY_PERIOD = 7 days;

IMintableERC20 public immutable STAKING_ASSET;
IRegistry public immutable REGISTRY;

Expand All @@ -143,9 +152,6 @@ contract StakingAssetHandler is IStakingAssetHandler, Ownable {
// ZKPassport constraints
string public validDomain;
string public validScope;
uint256 public validValidityPeriodInSeconds = 7 days;
uint8 public minAge = 18;
string[] internal excludedCountries;

constructor(StakingAssetHandlerArgs memory _args) Ownable(_args.owner) {
require(_args.depositsPerMint > 0, CannotMintZeroAmount());
Expand Down Expand Up @@ -181,12 +187,6 @@ contract StakingAssetHandler is IStakingAssetHandler, Ownable {
validDomain = _args.domain;
validScope = _args.scope;

excludedCountries = new string[](4);
excludedCountries[0] = CUB;
excludedCountries[1] = IRN;
excludedCountries[2] = PKR;
excludedCountries[3] = UKR;

skipBindCheck = _args.skipBindCheck;
skipMerkleCheck = _args.skipMerkleCheck;
}
Expand Down Expand Up @@ -323,18 +323,19 @@ contract StakingAssetHandler is IStakingAssetHandler, Ownable {
function _validatePassportProof(address _attester, ProofVerificationParams calldata _params) internal {
// Must NOT be using dev mode - https://docs.zkpassport.id/getting-started/dev-mode
// If active, nullifiers will end up being zero, but it is user provided input, so we are sanity checking it
require(_params.devMode == false, InvalidProof());
require(_params.serviceConfig.devMode == false, InvalidProof());

require(keccak256(bytes(_params.domain)) == keccak256(bytes(validDomain)), InvalidDomain());
require(keccak256(bytes(_params.scope)) == keccak256(bytes(validScope)), InvalidScope());
require(keccak256(bytes(_params.serviceConfig.domain)) == keccak256(bytes(validDomain)), InvalidDomain());
require(keccak256(bytes(_params.serviceConfig.scope)) == keccak256(bytes(validScope)), InvalidScope());
require(_params.serviceConfig.validityPeriodInSeconds == VALIDITY_PERIOD, InvalidValidityPeriod());

(bool verified, bytes32 nullifier) = zkPassportVerifier.verifyProof(_params);

require(verified, InvalidProof());
require(!nullifiers[nullifier], SybilDetected(nullifier));

if (!skipBindCheck) {
BoundData memory boundData = zkPassportVerifier.getBoundData(_params);
BoundData memory boundData = zkPassportVerifier.getBoundData(_params.commitments);

// Make sure the bound user address is the same as the _attester
require(boundData.senderAddress == _attester, InvalidBoundAddress(boundData.senderAddress, _attester));
Expand All @@ -343,19 +344,26 @@ contract StakingAssetHandler is IStakingAssetHandler, Ownable {
// Make sure the custom data is empty
require(bytes(boundData.customData).length == 0, ExtraDiscloseDataNonZero());

// Validity period check
require(validValidityPeriodInSeconds == _params.validityPeriodInSeconds, InvalidValidityPeriod());

// Age check
bool isAgeValid = zkPassportVerifier.isAgeAboveOrEqual(minAge, _params);
bool isAgeValid = zkPassportVerifier.isAgeAboveOrEqual(MIN_AGE, _params.commitments, _params.serviceConfig);
require(isAgeValid, InvalidAge());

// Country exclusion check
bool isCountryValid = zkPassportVerifier.isNationalityOut(excludedCountries, _params);
string[] memory excludedCountries = new string[](4);
excludedCountries[0] = CUB;
excludedCountries[1] = IRN;
excludedCountries[2] = PKR;
excludedCountries[3] = UKR;
bool isCountryValid = zkPassportVerifier.isNationalityOut(excludedCountries, _params.commitments);
require(isCountryValid, InvalidCountry());

// Sanctions check
zkPassportVerifier.enforceSanctionsRoot(_params);
zkPassportVerifier.enforceSanctionsRoot(_params.commitments);

// Face match check
bool isFaceMatchValid =
zkPassportVerifier.isFaceMatchVerified(FaceMatchMode.STRICT, OS.ANY, _params.commitments, _params.serviceConfig);
require(isFaceMatchValid, InvalidFaceMatch());
}

// Set nullifier to consumed
Expand Down
13 changes: 9 additions & 4 deletions l1-contracts/test/DateGatedRelayer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ contract DateGatedRelayerTest is Test {
uint256 gatedUntil = bound(_gatedUntil, block.timestamp + 1, type(uint32).max);

TestERC20 testERC20 = new TestERC20("test", "TEST", address(this));
CoinIssuer coinIssuer = new CoinIssuer(testERC20, 100, address(this));
testERC20.mint(address(this), 1e18);
CoinIssuer coinIssuer = new CoinIssuer(testERC20, 100e18, address(this));
testERC20.transferOwnership(address(coinIssuer));
coinIssuer.acceptTokenOwnership();

Expand All @@ -45,11 +46,15 @@ contract DateGatedRelayerTest is Test {
uint256 warp = bound(_warp, gatedUntil, type(uint32).max);

vm.expectRevert();
coinIssuer.mint(address(this), 100);
coinIssuer.mint(address(this), 1);

vm.warp(warp);
dateGatedRelayer.relay(address(coinIssuer), abi.encodeWithSelector(CoinIssuer.mint.selector, address(this), 100));
uint256 mintAvailable = coinIssuer.mintAvailable();
dateGatedRelayer.relay(
address(coinIssuer), abi.encodeWithSelector(CoinIssuer.mint.selector, address(this), mintAvailable)
);

assertEq(testERC20.balanceOf(address(this)), 100);
assertEq(testERC20.balanceOf(address(this)), mintAvailable + 1e18, "balanceOf");
assertEq(testERC20.totalSupply(), mintAvailable + 1e18, "totalSupply");
}
}
3 changes: 2 additions & 1 deletion l1-contracts/test/governance/coin-issuer/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ contract CoinIssuerBase is Test {

CoinIssuer internal nom;

function _deploy(uint256 _rate) internal {
function _deploy(uint256 _rate, uint256 _initialSupply) internal {
TestERC20 testERC20 = new TestERC20("test", "TEST", address(this));
token = IMintableERC20(address(testERC20));
token.mint(address(this), _initialSupply);
nom = new CoinIssuer(token, _rate, address(this));
testERC20.transferOwnership(address(nom));
nom.acceptTokenOwnership();
Expand Down
Loading
Loading