From 046a9f1c034ef48905ffb420deb4402c8f76d219 Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Tue, 16 Sep 2025 17:20:21 +0200 Subject: [PATCH 1/4] feat: limit `transferAndCall` to be called by the distributor address --- src/XanV1.sol | 17 +++++++++++------ src/libs/Locking.sol | 2 ++ test/XanV1.initialization.t.sol | 2 +- test/XanV1.locking.t.sol | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/XanV1.sol b/src/XanV1.sol index 30dad2a..52b284c 100644 --- a/src/XanV1.sol +++ b/src/XanV1.sol @@ -86,12 +86,13 @@ contract XanV1 is } /// @notice Initializes the XanV1 contract. - /// @param initialMintRecipient The initial recipient of the minted tokens. + /// @param distributor The distributor address being the initial recipient of the minted tokens and authorized + /// caller of the `transferAndLock` function. /// @param council The address of the governance council contract. - function initializeV1( /* solhint-disable-line comprehensive-interface*/ - address initialMintRecipient, - address council - ) external initializer { + function initializeV1( /* solhint-disable-line comprehensive-interface*/ address distributor, address council) + external + initializer + { // Initialize inherited contracts __ERC20_init({name_: Parameters.NAME, symbol_: Parameters.SYMBOL}); __ERC20Permit_init({name: Parameters.NAME}); @@ -99,7 +100,8 @@ contract XanV1 is __UUPSUpgradeable_init(); // Initialize the XanV1 contract - _mint(initialMintRecipient, Parameters.SUPPLY); + _mint(distributor, Parameters.SUPPLY); + _getLockingData().transferAndLockCaller = distributor; _getCouncilData().council = council; } @@ -110,6 +112,9 @@ contract XanV1 is /// @inheritdoc IXanV1 function transferAndLock(address to, uint256 value) external override { + if (_getLockingData().transferAndLockCaller != msg.sender) { + revert UnauthorizedCaller({caller: msg.sender}); + } _transfer({from: msg.sender, to: to, value: value}); _lock({account: to, value: value}); } diff --git a/src/libs/Locking.sol b/src/libs/Locking.sol index 758fddf..ab9fa09 100644 --- a/src/libs/Locking.sol +++ b/src/libs/Locking.sol @@ -9,8 +9,10 @@ library Locking { /// @notice A struct containing data associated with the token-locking mechanism. /// @param lockedBalances The locked balances associated with the current implementation. /// @param lockedSupply The locked total supply associated with the current implementation. + /// @param transferAndLockCaller The address being authorized to call the `transferAndLock` function. struct Data { mapping(address owner => uint256) lockedBalances; uint256 lockedSupply; + address transferAndLockCaller; } } diff --git a/test/XanV1.initialization.t.sol b/test/XanV1.initialization.t.sol index 0cb7cc5..80d2288 100644 --- a/test/XanV1.initialization.t.sol +++ b/test/XanV1.initialization.t.sol @@ -44,7 +44,7 @@ contract XanV1InitializationTest is Test { assertEq(uninitializedProxy.unlockedBalanceOf(_defaultSender), 0); - uninitializedProxy.initializeV1({initialMintRecipient: _defaultSender, council: _COUNCIL}); + uninitializedProxy.initializeV1({distributor: _defaultSender, council: _COUNCIL}); assertEq(uninitializedProxy.unlockedBalanceOf(_defaultSender), uninitializedProxy.totalSupply()); } diff --git a/test/XanV1.locking.t.sol b/test/XanV1.locking.t.sol index eb88ba7..c4e9e68 100644 --- a/test/XanV1.locking.t.sol +++ b/test/XanV1.locking.t.sol @@ -108,6 +108,23 @@ contract XanV1LockingTest is Test { _xanProxy.transferAndLock({to: _RECEIVER, value: 1}); } + function test_transferAndLock_reverts_if_the_caller_is_not_authorized(address other) public { + vm.assume(other != _defaultSender && other != address(0)); + + // Fund an address that is not the authorized `transferAndLock` caller. + uint256 value = 100; + { + vm.prank(_defaultSender); + _xanProxy.transfer(other, value); + assertEq(_xanProxy.unlockedBalanceOf(other), 100); + } + + // Attempt to call `transferAndLock` + vm.prank(other); + vm.expectRevert(abi.encodeWithSelector(XanV1.UnauthorizedCaller.selector, other), address(_xanProxy)); + _xanProxy.transferAndLock({to: _RECEIVER, value: value}); + } + function test_transfer_transfers_and_locks_tokens() public { uint256 supply = _xanProxy.totalSupply(); From c27932c0e823ea317b7789e4fbc7009be0c4fc31 Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Tue, 16 Sep 2025 17:31:06 +0200 Subject: [PATCH 2/4] doc: add natspec comment --- src/interfaces/IXanV1.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interfaces/IXanV1.sol b/src/interfaces/IXanV1.sol index 09c09f7..6f1027a 100644 --- a/src/interfaces/IXanV1.sol +++ b/src/interfaces/IXanV1.sol @@ -48,7 +48,8 @@ interface IXanV1 { /// @param value The value to lock. function lock(uint256 value) external; - /// @notice Transfers tokens and immediately locks them. + /// @notice Transfers tokens and immediately locks them. This function is only callable by the authorized token + /// distributor address. /// @param to The receiver. /// @param value The value to be transferred and locked. function transferAndLock(address to, uint256 value) external; From 8a1ff7fccd92d5fa1b816dc8ca5ce96a53b7be2e Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Tue, 16 Sep 2025 17:59:30 +0200 Subject: [PATCH 3/4] refactor: rename variable in the V2 draft --- src/drafts/XanV2.sol | 7 ++++--- test/drafts/XanV2Forwarder.t.sol | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/drafts/XanV2.sol b/src/drafts/XanV2.sol index 70ab3e1..a594500 100644 --- a/src/drafts/XanV2.sol +++ b/src/drafts/XanV2.sol @@ -32,12 +32,13 @@ contract XanV2 is IXanV2, XanV1 { } /// @notice Initializes the XanV2 contract. - /// @param initialMintRecipient The initial recipient of the minted tokens. + /// @param distributor The distributor address being the initial recipient of the minted tokens and authorized + /// caller of the `transferAndLock` function. /// @param council The address of the governance council contract. /// @param xanV2Forwarder The XanV2 forwarder contract. /// @custom:oz-upgrades-validate-as-initializer function initializeV2( /* solhint-disable-line comprehensive-interface*/ - address initialMintRecipient, + address distributor, address council, address xanV2Forwarder ) external reinitializer(2) { @@ -48,7 +49,7 @@ contract XanV2 is IXanV2, XanV1 { __UUPSUpgradeable_init(); // Initialize the XanV1 contract - _mint(initialMintRecipient, Parameters.SUPPLY); + _mint(distributor, Parameters.SUPPLY); _getCouncilData().council = council; // Initialize the XanV2 contract diff --git a/test/drafts/XanV2Forwarder.t.sol b/test/drafts/XanV2Forwarder.t.sol index f39ff65..bbd7f06 100644 --- a/test/drafts/XanV2Forwarder.t.sol +++ b/test/drafts/XanV2Forwarder.t.sol @@ -28,7 +28,7 @@ contract XanV2ForwarderUnitTest is Test { }); _xanV2Proxy.initializeV2({ - initialMintRecipient: _defaultSender, + distributor: _defaultSender, council: _governanceCouncil, xanV2Forwarder: address(_xanV2Forwarder) }); From 8d93b92864f5bdfd5688d65f8e87e178d2175e02 Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Thu, 18 Sep 2025 11:26:06 +0200 Subject: [PATCH 4/4] revert: rename distributor back to initialMintRecipient --- src/XanV1.sol | 16 ++++++++-------- src/drafts/XanV2.sol | 8 ++++---- test/XanV1.initialization.t.sol | 2 +- test/drafts/XanV2Forwarder.t.sol | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/XanV1.sol b/src/XanV1.sol index 52b284c..42c2316 100644 --- a/src/XanV1.sol +++ b/src/XanV1.sol @@ -86,13 +86,13 @@ contract XanV1 is } /// @notice Initializes the XanV1 contract. - /// @param distributor The distributor address being the initial recipient of the minted tokens and authorized - /// caller of the `transferAndLock` function. + /// @param initialMintRecipient The distributor address being the initial recipient of the minted tokens and + /// authorized caller of the `transferAndLock` function. /// @param council The address of the governance council contract. - function initializeV1( /* solhint-disable-line comprehensive-interface*/ address distributor, address council) - external - initializer - { + function initializeV1( /* solhint-disable-line comprehensive-interface*/ + address initialMintRecipient, + address council + ) external initializer { // Initialize inherited contracts __ERC20_init({name_: Parameters.NAME, symbol_: Parameters.SYMBOL}); __ERC20Permit_init({name: Parameters.NAME}); @@ -100,8 +100,8 @@ contract XanV1 is __UUPSUpgradeable_init(); // Initialize the XanV1 contract - _mint(distributor, Parameters.SUPPLY); - _getLockingData().transferAndLockCaller = distributor; + _mint(initialMintRecipient, Parameters.SUPPLY); + _getLockingData().transferAndLockCaller = initialMintRecipient; _getCouncilData().council = council; } diff --git a/src/drafts/XanV2.sol b/src/drafts/XanV2.sol index a594500..1d130b2 100644 --- a/src/drafts/XanV2.sol +++ b/src/drafts/XanV2.sol @@ -32,13 +32,13 @@ contract XanV2 is IXanV2, XanV1 { } /// @notice Initializes the XanV2 contract. - /// @param distributor The distributor address being the initial recipient of the minted tokens and authorized - /// caller of the `transferAndLock` function. + /// @param initialMintRecipient The distributor address being the initial recipient of the minted tokens and + /// authorized caller of the `transferAndLock` function. /// @param council The address of the governance council contract. /// @param xanV2Forwarder The XanV2 forwarder contract. /// @custom:oz-upgrades-validate-as-initializer function initializeV2( /* solhint-disable-line comprehensive-interface*/ - address distributor, + address initialMintRecipient, address council, address xanV2Forwarder ) external reinitializer(2) { @@ -49,7 +49,7 @@ contract XanV2 is IXanV2, XanV1 { __UUPSUpgradeable_init(); // Initialize the XanV1 contract - _mint(distributor, Parameters.SUPPLY); + _mint(initialMintRecipient, Parameters.SUPPLY); _getCouncilData().council = council; // Initialize the XanV2 contract diff --git a/test/XanV1.initialization.t.sol b/test/XanV1.initialization.t.sol index 80d2288..0cb7cc5 100644 --- a/test/XanV1.initialization.t.sol +++ b/test/XanV1.initialization.t.sol @@ -44,7 +44,7 @@ contract XanV1InitializationTest is Test { assertEq(uninitializedProxy.unlockedBalanceOf(_defaultSender), 0); - uninitializedProxy.initializeV1({distributor: _defaultSender, council: _COUNCIL}); + uninitializedProxy.initializeV1({initialMintRecipient: _defaultSender, council: _COUNCIL}); assertEq(uninitializedProxy.unlockedBalanceOf(_defaultSender), uninitializedProxy.totalSupply()); } diff --git a/test/drafts/XanV2Forwarder.t.sol b/test/drafts/XanV2Forwarder.t.sol index bbd7f06..f39ff65 100644 --- a/test/drafts/XanV2Forwarder.t.sol +++ b/test/drafts/XanV2Forwarder.t.sol @@ -28,7 +28,7 @@ contract XanV2ForwarderUnitTest is Test { }); _xanV2Proxy.initializeV2({ - distributor: _defaultSender, + initialMintRecipient: _defaultSender, council: _governanceCouncil, xanV2Forwarder: address(_xanV2Forwarder) });