Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8d0d97d
Fix names of files and implement swap tests
joaobrunoah Sep 5, 2025
24a63a5
Create hook logic and tests to verify signatures
joaobrunoah Sep 8, 2025
3480ff0
Fix signature test
joaobrunoah Sep 9, 2025
b354a32
Test error OnlyOncePerBlock
joaobrunoah Sep 9, 2025
385e966
100% coverage
joaobrunoah Sep 9, 2025
b2b6461
Block unbalanced liquidity operations when Angstrom is locked
joaobrunoah Sep 9, 2025
73c6b13
100% test coverage
joaobrunoah Sep 9, 2025
b48049d
Commit test
joaobrunoah Sep 9, 2025
226cf6f
Run github action
joaobrunoah Sep 9, 2025
d955e5a
Remove unused dependency
joaobrunoah Sep 9, 2025
a6742b9
Natspec
joaobrunoah Sep 9, 2025
77e9d60
Fix solady install
joaobrunoah Sep 9, 2025
78715cf
Fix lint
joaobrunoah Sep 10, 2025
1be7f44
Fix function mutability
joaobrunoah Sep 10, 2025
d08401b
Merge pull request #4 from balancer/angstrom-hook
joaobrunoah Sep 10, 2025
2c21fea
Modify package.json to Angstrom
joaobrunoah Sep 10, 2025
3fedf8e
Readme
joaobrunoah Sep 10, 2025
42f0474
Fix contract names and repo
joaobrunoah Sep 10, 2025
d0e5949
docs: update comments and non-semantics
EndymionJkb Sep 11, 2025
c770e11
refactor: rename `_nodes` for clarity
EndymionJkb Sep 11, 2025
5a325d7
refactor: add `isRegisteredNode` getter; add `fromValidator` modifier…
EndymionJkb Sep 11, 2025
e841b11
refactor: add onlyWhenLocked modifier, and isolate updates in `_unloc…
EndymionJkb Sep 11, 2025
49fd080
refactor: rename validator modifier; simplify unlockAngstrom helper
EndymionJkb Sep 11, 2025
b63265a
refactor: simplify/rename unlock functions; move getters from mock; r…
EndymionJkb Sep 11, 2025
28b2500
test: don't need leading underscores in tests
EndymionJkb Sep 11, 2025
f6bbc67
test: introduce intermediate test
EndymionJkb Sep 11, 2025
793e9a9
fix: createHook in proper place
EndymionJkb Sep 12, 2025
5b41904
test: propagate intermediate test
EndymionJkb Sep 12, 2025
6632cdd
refactor: rename validator modifier; simplify onBeforeSwap
EndymionJkb Sep 12, 2025
5603a14
refactor: remove redundant code
EndymionJkb Sep 12, 2025
4804023
refactor: rename/reorder
EndymionJkb Sep 12, 2025
124194a
refactor: remove unused function
EndymionJkb Sep 12, 2025
1ca3856
refactor: one more simplification - only revert in one place for each…
EndymionJkb Sep 12, 2025
d5b569c
docs: adjust comments after review
EndymionJkb Sep 16, 2025
6b24bc5
Merge pull request #6 from balancer/angstrom-updates-v2
EndymionJkb Sep 16, 2025
459f82a
Merge pull request #7 from balancer/support-hardhat-contracts
joaobrunoah Sep 16, 2025
94c94ac
Fix PR comment
joaobrunoah Sep 17, 2025
d23b3f8
Add event when adding/removing node
joaobrunoah Sep 17, 2025
ecc153e
Use OwnableAuthentication instead of Singleton
joaobrunoah Sep 17, 2025
b8f11ee
Use mcopy instead of a for loop
joaobrunoah Sep 17, 2025
b257566
Update contracts/AngstromBalancer.sol
joaobrunoah Sep 17, 2025
f26e69e
Unify invalid signature errors
joaobrunoah Sep 17, 2025
657ac83
Change toggle to add/remove
joaobrunoah Sep 17, 2025
418d2a0
Revert add/removeNode if node state is inconsistent
joaobrunoah Sep 17, 2025
2f88693
Fix remappings
joaobrunoah Sep 17, 2025
001b761
Fix Angstrom to use only memory signatures
joaobrunoah Sep 18, 2025
eba2693
Add comments
joaobrunoah Sep 18, 2025
e6a55a5
Use unlockAngstromWithSignature directly
joaobrunoah Sep 18, 2025
7af693d
Apply suggestions from code review
joaobrunoah Sep 18, 2025
c0cedc6
Fix register/deregister nodes
joaobrunoah Sep 18, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ pragma solidity ^0.8.24;

import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol";

import { IBatchRouterQueries } from "@balancer-labs/v3-interfaces/contracts/vault/IBatchRouterQueries.sol";
import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol";
import { IRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IRouter.sol";
import { IBatchRouter } from "@balancer-labs/v3-interfaces/contracts/vault/IBatchRouter.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import {
SwapPathExactAmountIn,
Expand All @@ -18,8 +19,7 @@ import { SingletonAuthentication } from "@balancer-labs/v3-vault/contracts/Singl
import { BatchRouterHooks } from "@balancer-labs/v3-vault/contracts/BatchRouterHooks.sol";
import { RouterCommon } from "@balancer-labs/v3-vault/contracts/RouterCommon.sol";

// TODO interface
contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
contract AngstromRouterAndHook is IBatchRouter, BatchRouterHooks, SingletonAuthentication {
uint256 internal _lastUnlockBlockNumber;

error OnlyOncePerBlock();
Expand All @@ -39,7 +39,7 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
/***************************************************************************
Swaps
***************************************************************************/

/// @inheritdoc IBatchRouter
function swapExactIn(
SwapPathExactAmountIn[] memory paths,
uint256 deadline,
Expand All @@ -51,8 +51,8 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
saveSender(msg.sender)
returns (uint256[] memory pathAmountsOut, address[] memory tokensOut, uint256[] memory amountsOut)
{
// Unlocks the router in this block. If the router is already unlocked, reverts.
_unlockRouter();
// Unlocks the Angstrom network in this block. If the Angstrom network is already unlocked, reverts.
_unlockAngstrom();

return
abi.decode(
Expand All @@ -72,6 +72,7 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
);
}

/// @inheritdoc IBatchRouter
function swapExactOut(
SwapPathExactAmountOut[] memory paths,
uint256 deadline,
Expand All @@ -83,8 +84,8 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
saveSender(msg.sender)
returns (uint256[] memory pathAmountsIn, address[] memory tokensIn, uint256[] memory amountsIn)
{
// Unlocks the router in this block. If the router is already unlocked, reverts.
_unlockRouter();
// Unlocks the Angstrom Network in this block. If the Angstrom Network is already unlocked, reverts.
_unlockAngstrom();

return
abi.decode(
Expand All @@ -108,6 +109,7 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
Queries
***************************************************************************/

/// @inheritdoc IBatchRouterQueries
function querySwapExactIn(
SwapPathExactAmountIn[] memory paths,
address sender,
Expand Down Expand Up @@ -139,6 +141,7 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
);
}

/// @inheritdoc IBatchRouterQueries
function querySwapExactOut(
SwapPathExactAmountOut[] memory paths,
address sender,
Expand Down Expand Up @@ -174,11 +177,10 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
Nodes Management
***************************************************************************/

function addNode(address node) external authenticate {
_nodes[node] = true;
}
function removeNode(address node) external authenticate {
_nodes[node] = false;
function toggleNodes(address[] memory nodes) external authenticate {
for (uint256 i = 0; i < nodes.length; i++) {
_nodes[nodes[i]] = !_nodes[nodes[i]];
}
}

/***************************************************************************
Expand All @@ -193,9 +195,9 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
Private Functions
***************************************************************************/

function _unlockRouter() internal {
function _unlockAngstrom() internal {
// The router can only be unlocked once per block.
if (_isRouterUnlocked()) {
if (_isAngstromUnlocked()) {
revert OnlyOncePerBlock();
}

Expand All @@ -211,7 +213,7 @@ contract AngstromRouter is BatchRouterHooks, SingletonAuthentication {
return _nodes[account];
}

function _isRouterUnlocked() internal view returns (bool) {
function _isAngstromUnlocked() internal view returns (bool) {
return _lastUnlockBlockNumber == block.number;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import { IPermit2 } from "permit2/src/interfaces/IPermit2.sol";
import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/misc/IWETH.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";

import { AngstromRouter } from "../AngstromRouter.sol";
import { AngstromRouterAndHook } from "../AngstromRouterAndHook.sol";

contract AngstromRouterMock is AngstromRouter {
contract AngstromRouterAndHookMock is AngstromRouterAndHook {
constructor(
IVault vault,
IWETH weth,
IPermit2 permit2,
string memory routerVersion
) AngstromRouter(vault, weth, permit2, routerVersion) {
) AngstromRouterAndHook(vault, weth, permit2, routerVersion) {
// solhint-disable-previous-line no-empty-blocks
}

function manualUnlockRouter() external {
_unlockRouter();
function manualUnlockAngstrom() external {
_unlockAngstrom();
}

function getLastUnlockBlockNumber() external view returns (uint256) {
Expand Down
96 changes: 96 additions & 0 deletions test/foundry/AngstromRouterAndHook.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import "forge-std/Test.sol";

import { IAuthentication } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IAuthentication.sol";
import {
SwapPathExactAmountIn,
SwapPathExactAmountOut
} from "@balancer-labs/v3-interfaces/contracts/vault/BatchRouterTypes.sol";

import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol";
import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol";

import { AngstromRouterAndHookMock } from "../../contracts/test/AngstromRouterAndHookMock.sol";
import { AngstromRouterAndHook } from "../../contracts/AngstromRouterAndHook.sol";

contract AngstromRouterAndHookTest is BaseVaultTest {
using ArrayHelpers for *;

AngstromRouterAndHookMock private _angstromRouterAndHook;

function setUp() public virtual override {
super.setUp();

_angstromRouterAndHook = new AngstromRouterAndHookMock(vault, weth, permit2, "AngstromRouterAndHook Mock v1");

authorizer.grantRole(_angstromRouterAndHook.getActionId(AngstromRouterAndHook.toggleNodes.selector), admin);
}

function testSwapExactInNotNode() public {
SwapPathExactAmountIn[] memory paths;
vm.expectRevert(AngstromRouterAndHook.NotNode.selector);
_angstromRouterAndHook.swapExactIn(paths, MAX_UINT256, false, bytes(""));
}

function testSwapExactInAlreadyUnlocked() public {
vm.prank(admin);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());

SwapPathExactAmountIn[] memory paths;

vm.startPrank(bob);
_angstromRouterAndHook.manualUnlockAngstrom();
vm.expectRevert(AngstromRouterAndHook.OnlyOncePerBlock.selector);
_angstromRouterAndHook.swapExactIn(paths, MAX_UINT256, false, bytes(""));
vm.stopPrank();
}

function testSwapExactInUnlocksAngstrom() public {
vm.prank(admin);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());

SwapPathExactAmountIn[] memory paths;
vm.prank(bob);
_angstromRouterAndHook.swapExactIn(paths, MAX_UINT256, false, bytes(""));
assertEq(
_angstromRouterAndHook.getLastUnlockBlockNumber(),
block.number,
"Last unlock block number is not the current block number"
);
}

function testSwapExactOutNotNode() public {
SwapPathExactAmountOut[] memory paths;
vm.expectRevert(AngstromRouterAndHook.NotNode.selector);
_angstromRouterAndHook.swapExactOut(paths, MAX_UINT256, false, bytes(""));
}

function testSwapExactOutAlreadyUnlocked() public {
vm.prank(admin);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());

SwapPathExactAmountOut[] memory paths;
vm.startPrank(bob);
_angstromRouterAndHook.manualUnlockAngstrom();
vm.expectRevert(AngstromRouterAndHook.OnlyOncePerBlock.selector);
_angstromRouterAndHook.swapExactOut(paths, MAX_UINT256, false, bytes(""));
vm.stopPrank();
}

function testSwapExactOutUnlocksRouter() public {
vm.prank(admin);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());

SwapPathExactAmountOut[] memory paths;
vm.prank(bob);
_angstromRouterAndHook.swapExactOut(paths, MAX_UINT256, false, bytes(""));
assertEq(
_angstromRouterAndHook.getLastUnlockBlockNumber(),
block.number,
"Last unlock block number is not the current block number"
);
}
}
84 changes: 84 additions & 0 deletions test/foundry/AngstromRouterAndHookUnit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import "forge-std/Test.sol";

import { IAuthentication } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/helpers/IAuthentication.sol";

import { ArrayHelpers } from "@balancer-labs/v3-solidity-utils/contracts/test/ArrayHelpers.sol";
import { BaseVaultTest } from "@balancer-labs/v3-vault/test/foundry/utils/BaseVaultTest.sol";

import { AngstromRouterAndHookMock } from "../../contracts/test/AngstromRouterAndHookMock.sol";
import { AngstromRouterAndHook } from "../../contracts/AngstromRouterAndHook.sol";

contract AngstromRouterAndHookUnitTest is BaseVaultTest {
using ArrayHelpers for *;

AngstromRouterAndHookMock private _angstromRouterAndHook;

function setUp() public virtual override {
super.setUp();

_angstromRouterAndHook = new AngstromRouterAndHookMock(vault, weth, permit2, "AngstromRouterAndHook Mock v1");

authorizer.grantRole(_angstromRouterAndHook.getActionId(AngstromRouterAndHook.toggleNodes.selector), admin);
}

function testToggleNodesIsAuthenticated() public {
vm.expectRevert(IAuthentication.SenderNotAllowed.selector);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());
}

function testToggleNodes() public {
vm.prank(admin);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());
assertTrue(_angstromRouterAndHook.isNode(bob), "Bob is not a node");
}

function testAddAndRemoveNodes() public {
vm.startPrank(admin);
_angstromRouterAndHook.toggleNodes([bob, alice].toMemoryArray());
assertTrue(_angstromRouterAndHook.isNode(bob), "Bob is not a node");
assertTrue(_angstromRouterAndHook.isNode(alice), "Alice is not a node");
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());
vm.stopPrank();
assertFalse(_angstromRouterAndHook.isNode(bob), "Bob is still a node");
assertTrue(_angstromRouterAndHook.isNode(alice), "Alice is not a node after bob was removed");
}

function testUnlockAngstromNotNode() public {
vm.expectRevert(AngstromRouterAndHook.NotNode.selector);
_angstromRouterAndHook.manualUnlockAngstrom();
}

function testUnlockAngstromTwice() public {
vm.prank(admin);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());

vm.startPrank(bob);
_angstromRouterAndHook.manualUnlockAngstrom();
vm.expectRevert(AngstromRouterAndHook.OnlyOncePerBlock.selector);
_angstromRouterAndHook.manualUnlockAngstrom();
vm.stopPrank();
}

function testUnlockAngstromSetsLastUnlockBlockNumber() public {
vm.prank(admin);
_angstromRouterAndHook.toggleNodes([bob].toMemoryArray());

assertEq(_angstromRouterAndHook.getLastUnlockBlockNumber(), 0, "Last unlock block number is not 0");

vm.prank(bob);
_angstromRouterAndHook.manualUnlockAngstrom();
assertEq(
_angstromRouterAndHook.getLastUnlockBlockNumber(),
block.number,
"Last unlock block number is not the current block number"
);
}

function testGetVault() public {
assertEq(address(_angstromRouterAndHook.getVault()), address(vault), "Wrong vault address");
}
}
Loading