Skip to content

Conversation

arnigudj
Copy link
Member

@arnigudj arnigudj commented Aug 22, 2025

New Validator contract and wire V1 block for Balancer V3

Scope

  • Context, Balancer V3 issue:

    Balancer V3 uses a cash till pattern. Operations settle at the end of the transaction. With double entrypoint tokens that share state, like EUReV1 and EUReV2, you can pay once and get credit twice. If two pools exist, one can be drained.
    We can block the EURe V1 frontend from touching Balancer V3 by upgrading the Validator used by EURe V2. Balancer V3 settles via a single Vault address, so blocking that address prevents all new V3 pools from using V1.
    EURe V1 is not upgradeable, but every EURe V1 state change goes through EURe V2’s *_withCaller entrypoints, so EURe V2 can refuse Balancer-related calls. Block both transfer and transferFrom to the V3 Vault.

V1 Frontend using V2 Proxy as controller
Figure: V1 Frontend using V2 Proxy as controller

  • Add src/Validator.sol that V2 calls before state changes.

  • Roles:

    • ADMIN_ROLE manages other roles.
    • V1_FRONTEND_ROLE marks the chain’s V1 frontend.
    • V1_BLOCKED_ROLE marks counterparties V1 must not touch, for example the Balancer V3 Vault.
    • BLACKLISTED_ROLE blocks addresses globally.
  • Tests in test/Validator.t.sol. We have 100% coverage for Validator.

Implementation

  • Validator is a pure view gate: validate(from, to, amount) -> bool.

/**
* @dev Validates a transfer between two accounts.
* - If called by a V1 frontend, checks if either account is blocked.
* - Always checks if either account is blacklisted.
* @return valid True if transfer is allowed, false otherwise.
*/
function validate(
address from,
address to,
uint256 /* amount */
) external view override returns (bool valid) {
if (isV1Frontend(msg.sender)) {
if (isV1Blocked(from)) {
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(from),
" is blocked in V1. Please use V2 instead. See https://monerium.dev/docs/tokens"
)
)
);
}
if (isV1Blocked(to)) {
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(to),
" is blocked in V1. Please use V2 instead. See https://monerium.dev/docs/tokens"
)
)
);
}
}
if (isBlacklisted(from)) {
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(from),
" is blacklisted."
)
)
);
}
if (isBlacklisted(to)) {
revert(
string(
abi.encodePacked(
"Transfer not supported:",
Strings.toHexString(to),
" is blacklisted."
)
)
);
}
return true;
}

  • For V1 transfers, the front end will always be the msg.sender. Because it's using the*_withCaller entrypoints in V2.
  • If msg.sender has V1_FRONTEND_ROLE, return false when from or to has V1_BLOCKED_ROLE.
  • Always return false when from or to has BLACKLISTED_ROLE.
  • DEFAULT_ADMIN_ROLE granted to deployer. ADMIN_ROLE is admin for all other roles.
  • Helpers: isAdminAccount, isV1Blocked, isBlacklisted, isV1Frontend.
  • Contract identity: CONTRACT_ID() for downstream checks by the V2 when setting the new Validator.
  • V2 integration, the controller calls validator.validate(...) before moving balances. Example from src/controllers/GnosisController.sol:

function transfer_withCaller(
address caller,
address to,
uint256 amount
) external onlyFrontend returns (bool) {
require(
validator.validate(caller, to, amount),
"Transfer not validated"
);
_transfer(caller, to, amount);
return true;
}
function transferFrom_withCaller(
address caller,
address from,
address to,
uint256 amount
) external onlyFrontend returns (bool) {
require(validator.validate(from, to, amount), "Transfer not validated");
_spendAllowance(from, caller, amount);
_transfer(from, to, amount);
return true;
}

  • src/Token.sol setter (already present), used to switch to the new validator:

    // Function to set the validator, restricted to owner
    function setValidator(address _validator) public onlyOwner {
    validator = IValidator(_validator);
    require(validator.CONTRACT_ID() == keccak256("monerium.validator"), "Not Monerium Validator Contract");
    }

How to Test

Install

yarn install
forge install

Unit tests

forge test --match-contract '^ValidatorTest'

Coverage

forge coverage --match-contract ^ValidatorTest

Sample output:

Ran 10 tests for test/Validator.t.sol:ValidatorTest
[PASS] testAdminRole() (gas: 24953)
[PASS] testBlacklistedRole() (gas: 24995)
[PASS] testBlockedRole() (gas: 25018)
[PASS] testContractId() (gas: 5859)
[PASS] testFrontendRole() (gas: 25060)
[PASS] testIsAdminAccount() (gas: 20778)
[PASS] testIsBlacklisted() (gas: 14912)
[PASS] testIsV1Blocked() (gas: 14871)
[PASS] testIsV1Frontend() (gas: 14804)
[PASS] testValidateTransfer() (gas: 53251)
Suite result: ok. 10 passed; 0 failed; 0 skipped; finished in 16.83ms (8.65ms CPU time)

Ran 1 test suite in 182.94ms (16.83ms CPU time): 10 tests passed, 0 failed, 0 skipped (10 total tests)

╭---------------------------+-----------------+-----------------+---------------+-----------------╮
| File                      | % Lines         | % Statements    | % Branches    | % Funcs         |
+=================================================================================================+
| src/Validator.sol         | 100.00% (39/39) | 100.00% (30/30) | 100.00% (3/3) | 100.00% (15/15) |
╰---------------------------+-----------------+-----------------+---------------+-----------------╯

We have 100% coverage for the validator.

Deployment plan (Gnosis first)

  1. Deploy Validator.
  2. Grant admin roles: setAdmin(<monerium admins>)
  3. Set EURe V1 frontend setV1Frontend(0xcB444e90D8198415266c6a2724b7900fb12FC56E)
  4. Block balancer V3 Vault setV1Blocked(0xbA1333333333a1BA1108E8412f11850A5C319bA9)
  5. Point V2 to the new validator: Token.setValidator(<validatorAddress>)
  6. Verify by attempting:
    • A V1-originating transfer to the Balancer V3 Vault, expect revert.
    • A normal V1 transfer to a non-blocked address, expect success.
    • A V2-originating transfer to the Balancer V3 Vault, expect sucess.
    • A normal V2 transfer to a non-blocked address, expect success.

@arnigudj arnigudj changed the title Add Validator contract and wire V1 block for Balancer V3 New Validator contract and wire V1 block for Balancer V3 Aug 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant