Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"solidity.remappings": [
"ds-test/=lib/forge-std/lib/ds-test/src/",
"forge-std/=lib/forge-std/src",
"src/=./src/",
"permit2/=lib/permit2/",
"solmate/=lib/solmate/"
]
}
9 changes: 9 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@ src = "src"
out = "out"
libs = ["lib"]

optimizer = true
optimizer_runs = 10_000

# Configs to support CREATE2
solc = "0.8.29" # Pin solidity version
evm_version = "paris" # Because PUSH0 may not be supported everywhere
bytecode_hash = "none" # Avoid bytecode hash to prevent changes to bytecode
cbor_metadata = false # Disable CBOR metadata

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
19 changes: 0 additions & 19 deletions script/Counter.s.sol

This file was deleted.

39 changes: 39 additions & 0 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;

import {Script, console} from "forge-std/Script.sol";
import {BungeeApproveAndBridge} from "src/BungeeApproveAndBridge.sol";

contract DeployScript is Script {
BungeeApproveAndBridge public bungeeApproveAndBridge;

address constant SOCKET_GATEWAY = 0x3a23F943181408EAC424116Af7b7790c94Cb97a5;

function run() public {
address txOrigin = msg.sender;
address create2Deployer = 0x4e59b44847b379578588920cA78FbF26c0B4956C; // foundry uses this contract by default
bytes32 salt = keccak256(abi.encode(uint256(9999999999999999999999))); // change this before deploying if needed
console.log("Deployer: ", txOrigin);
console.log("Create2 Deployer: ", create2Deployer);
console.logBytes32(salt);

address computedAddress = vm.computeCreate2Address(
salt,
keccak256(abi.encodePacked(type(BungeeApproveAndBridge).creationCode, abi.encode(SOCKET_GATEWAY))),
create2Deployer
);
console.log("Computed address: ", computedAddress);

bungeeApproveAndBridge = BungeeApproveAndBridge(deploy(salt));
console.log("Deployed BungeeApproveAndBridge at: ", address(bungeeApproveAndBridge));
}

function deploy(bytes32 salt) public returns (address addr) {
vm.broadcast();
BungeeApproveAndBridge _bungeeApproveAndBridge = new BungeeApproveAndBridge{salt: salt}(SOCKET_GATEWAY);
addr = address(_bungeeApproveAndBridge);
console.log("Deployed contract to: %s", address(addr));

return addr;
}
}
171 changes: 171 additions & 0 deletions src/BungeeApproveAndBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;

import {ApproveAndBridge, IERC20} from "./mixin/ApproveAndBridge.sol";
import {Math} from "./vendored/Math.sol";

/// ! @dev UNAUDITED UNTESTED Do not use in production
/// @dev Performs two steps before bridging via SocketGateway:
/// 1. Modify input amount in calldata
/// 2. Modify output amount in calldata
/// 3. Call SocketGateway.fallback() with the modified calldata
contract BungeeApproveAndBridge is ApproveAndBridge {
error InvalidInput();
error PositionOutOfBounds();
error BridgeFailed();

/// @dev ModifyCalldataParams is a struct that contains information required to modify SocketGateway calldata
/// @dev the input amount index, modify output flag, and output amount index
struct ModifyCalldataParams {
uint256 inputAmountIdx;
bool modifyOutput;
uint256 outputAmountIdx;
}

/// @dev routeIds on SocketGateway are 4 bytes
uint8 private constant ROUTE_ID_BYTES_LENGTH = 4;
/// @dev there are 3 params in ModifyCalldataParams
uint8 private constant MODIFY_CALLDATA_PARAMS_COUNT = 3;
/// @dev each ModifyCalldataParams is 32 bytes
uint8 private constant MODIFY_CALLDATA_LENGTH_BYTES = 32;
/// @dev total length of the modify calldata bytes
uint8 private constant MODIFY_CALLDATA_LENGTH = MODIFY_CALLDATA_PARAMS_COUNT * MODIFY_CALLDATA_LENGTH_BYTES;
/// @dev minimum length of the data payload
/// @dev should atleast include the routeId and the ModifyCalldataParams
uint8 private constant MIN_DATA_LENGTH = ROUTE_ID_BYTES_LENGTH + MODIFY_CALLDATA_LENGTH;

/// @dev SocketGateway address
address public immutable SOCKET_GATEWAY;

constructor(address socketGateway_) {
require(socketGateway_.code.length > 0, "Socket gateway contract not deployed");

SOCKET_GATEWAY = socketGateway_;
}

/**
* @notice Approval should be given to the SocketGateway address
* @dev Returns the SocketGateway address
*/
function bridgeApprovalTarget() public view override returns (address) {
return address(SOCKET_GATEWAY);
}

/**
* @notice Bridge the token via SocketGateway
* @dev Modifies SocketGateway calldata to modify the input and output amounts before bridging
* @param token The token to bridge
* @param amount The amount of token to bridge
* @param nativeTokenExtraFee extra fee in native token, if any
* @param data encoded bytes including SocketGateway calldata and ModifyCalldataParams
*/
function bridge(IERC20 token, uint256 amount, uint256 nativeTokenExtraFee, bytes calldata data) internal override {
// decode & parse data to find positions in calldata to modify
bytes memory modifiedCalldata = _parseAndModifyCalldata(amount, data);

// execute using the modified calldata via SocketGateway.fallback()
(bool success,) = address(token) == NATIVE_TOKEN_ADDRESS
? address(SOCKET_GATEWAY).call{value: amount + nativeTokenExtraFee}(modifiedCalldata)
: address(SOCKET_GATEWAY).call{value: nativeTokenExtraFee}(modifiedCalldata);
if (!success) revert BridgeFailed();
}

/**
* @dev Parses and modifies the calldata to modify the input and output amounts before bridging
* @param amount Updated input amount to use to modify the calldata
* @param data encoded bytes including SocketGateway calldata and ModifyCalldataParams
* @return modifiedCalldata The modified calldata
*/
function _parseAndModifyCalldata(uint256 amount, bytes calldata data) internal pure returns (bytes memory) {
// Parse the data into route calldata and ModifyCalldataParams
(bytes memory routeCalldata, ModifyCalldataParams memory modifyCalldataParams) = _parseCalldata(data);

// Read the original input amount from the calldata
// before modifying input amount
uint256 originalInput = _readUint256({_data: routeCalldata, _index: modifyCalldataParams.inputAmountIdx});

// Replace the input amount in the calldata
bytes memory modifiedCalldata =
_replaceUint256({_original: routeCalldata, _start: modifyCalldataParams.inputAmountIdx, _amount: amount});

// Optionally replace the output amount if required
// in case of bridges like Across, need to modify both input and output amounts
// - decode current input and output amounts from calldata
// - calculate and apply the percentage diff bw new and old input amount on the old output amount
// - replace the output amount at the index with the new amount
// - assumes output amount is always uint256 in SocketGateway impls
if (modifyCalldataParams.modifyOutput) {
uint256 originalOutput = _readUint256({_data: routeCalldata, _index: modifyCalldataParams.outputAmountIdx});
uint256 newOutput = _applyPctDiff({_base: originalInput, _compare: amount, _target: originalOutput});
modifiedCalldata = _replaceUint256({
_original: modifiedCalldata,
_start: modifyCalldataParams.outputAmountIdx,
_amount: newOutput
});
}

return modifiedCalldata;
}

/**
* @dev Parses the calldata to extract the route calldata and ModifyCalldataParams
* @param _data The calldata to parse
* @return routeCalldata The SocketGateway route calldata
* @return modifyCalldataParams The ModifyCalldataParams
*/
function _parseCalldata(bytes calldata _data) internal pure returns (bytes memory, ModifyCalldataParams memory) {
// calldata should have minimum of routeId and ModifyCalldataParams
if (_data.length < MIN_DATA_LENGTH) revert InvalidInput();
uint256 routeCalldataLength = _data.length - MODIFY_CALLDATA_LENGTH;

// Extract the route execution calldata
bytes memory routeCalldata = _data[:routeCalldataLength];

// Extract the ModifyCalldataParams
ModifyCalldataParams memory modifyCalldataParams;
(modifyCalldataParams.inputAmountIdx, modifyCalldataParams.modifyOutput, modifyCalldataParams.outputAmountIdx) =
abi.decode(_data[routeCalldataLength:], (uint256, bool, uint256));

return (routeCalldata, modifyCalldataParams);
}

/**
* @dev Replaces a uint256 at a given position in a bytes data with a new uint256
* @dev Directly modifies the original bytes data in-place without creating a new copy
*/
function _replaceUint256(bytes memory _original, uint256 _start, uint256 _amount)
internal
pure
returns (bytes memory)
{
// check if the _start is out of bounds
if (_start + 32 > _original.length) revert PositionOutOfBounds();

// Directly modify externalData in-place without creating a new copy
assembly {
// Calculate position in memory where we need to write the new amount
// Write the amount at that position
mstore(add(add(_original, 32), _start), _amount)
}

return _original;
}
Comment on lines +136 to +152
Copy link

@coderabbitai coderabbitai bot Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Assembly operations look correct but need comprehensive testing.

The _replaceUint256 function uses assembly to modify memory in-place. The bounds check and memory offset calculations appear correct, but this low-level code requires thorough testing.

Consider adding unit tests specifically for this function to verify:

  • Correct replacement at various positions
  • Proper bounds checking behavior
  • Memory safety with different input sizes

🏁 Script executed:

#!/bin/bash
# Search for existing tests covering _replaceUint256 function
rg -A 10 -B 5 "_replaceUint256" --type sol
rg -n "_replaceUint256.*test" .

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Search for occurrences of _replaceUint256 across all Solidity files
rg -n "_replaceUint256" -g "*.sol"

# Search for tests referencing the function in JS/TS test files
rg -n "_replaceUint256" -g "test/**/*.js" -g "test/**/*.ts"

Length of output: 600


Add comprehensive unit tests for _replaceUint256

I didn’t find any existing tests covering this function in src/BungeeApproveAndBridge.sol, so please add dedicated unit tests to ensure its correctness and safety:

• Verify correct replacement of a 32-byte word at various byte offsets (start, middle, end)
• Confirm that _start + 32 > original.length triggers the PositionOutOfBounds() revert
• Exercise different input lengths (empty, exactly 32 bytes, larger payloads) to catch any memory-safety issues

File to cover:

  • src/BungeeApproveAndBridge.sol lines 134–150
🤖 Prompt for AI Agents
In src/BungeeApproveAndBridge.sol around lines 134 to 150, the function
_replaceUint256 lacks unit tests. Add comprehensive tests that verify correct
replacement of a 32-byte word at various offsets including start, middle, and
end of the input bytes. Also, test that the function reverts with
PositionOutOfBounds when _start + 32 exceeds the input length. Include tests
with different input lengths such as empty bytes, exactly 32 bytes, and larger
payloads to ensure memory safety and correctness.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests for _replaceUint256: f35c950

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@sebastiantf Perfect! I can see you've added comprehensive tests for _replaceUint256 that cover exactly what was needed:

Bounds checking: Tests verify PositionOutOfBounds revert when _start + 32 > length
Various offsets: Tests replacement at different positions within the bytes array
Edge cases: Tests with empty bytes, exact 32-byte arrays, and larger payloads
Memory safety: Tests ensure no corruption outside the replaced segment
Multiple replacements: Tests verify the function works correctly for sequential modifications

The test coverage looks thorough and addresses all the memory safety concerns I raised. Thanks for implementing these comprehensive tests!

(\   /)

( ..)
o
(")(") Great test coverage!


🏁 Script executed:

#!/bin/bash
# Look for tests related to _replaceUint256 function
rg -A 15 -B 5 "_replaceUint256" --type sol

# Check if there are test functions specifically for _replaceUint256
rg -A 10 "test.*replaceUint256" --type sol

# Look at the current test structure in BungeeApproveAndBridge test file
fd "BungeeApproveAndBridge.t.sol" --exec cat {}

Length of output: 70979


@sebastiantf Perfect! I can see you've added comprehensive tests for _replaceUint256 that cover exactly what was needed:

Bounds checking: Tests verify PositionOutOfBounds revert when _start + 32 > length
Various offsets: Tests replacement at different positions within the bytes array
Edge cases: Tests with empty bytes, exact 32-byte arrays, and larger payloads
Memory safety: Tests ensure no corruption outside the replaced segment
Multiple replacements: Tests verify the function works correctly for sequential modifications

The test coverage looks thorough and addresses all the memory safety concerns I raised. Thanks for implementing these comprehensive tests!

(\   /)

( ..)
o
(")(") Great test coverage!


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!


/**
* @dev Reads a uint256 at a given byte index in a bytes array
*/
function _readUint256(bytes memory _data, uint256 _index) internal pure returns (uint256 value) {
if (_data.length < _index + 32) revert PositionOutOfBounds();
assembly {
value := mload(add(add(_data, 0x20), _index))
}
}

/**
* @dev Applies a percentage difference to a target number
*/
function _applyPctDiff(uint256 _base, uint256 _compare, uint256 _target) internal pure returns (uint256) {
if (_base == 0) revert InvalidInput();
return Math.mulDiv({x: _target, y: _compare, denominator: _base});
}
}
14 changes: 0 additions & 14 deletions src/Counter.sol

This file was deleted.

11 changes: 11 additions & 0 deletions src/interface/IApproveAndBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;

import {IERC20} from "../vendored/IERC20.sol";

interface IApproveAndBridge {
function approveAndBridge(IERC20 token, uint256 minAmount, uint256 nativeTokenExtraFee, bytes calldata data)
external;

function bridgeApprovalTarget() external view returns (address);
}
54 changes: 54 additions & 0 deletions src/mixin/ApproveAndBridge.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8;

import {IApproveAndBridge} from "../interface/IApproveAndBridge.sol";
import {IERC20} from "../vendored/IERC20.sol";
import {SafeERC20} from "../vendored/SafeERC20.sol";

abstract contract ApproveAndBridge is IApproveAndBridge {
using SafeERC20 for IERC20;

error MinAmountNotMet();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to make a test for premature hook execution

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added some more unit tests here: 89a6929


/// @dev Address used to represent the native token
address public constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

/**
* @dev This function isn't intended to be called directly, it should be delegatecalled instead.
* @param token The token to bridge
* @param minAmount The minimum amount of tokens to bridge. minAmount should not be too small if the sell amount is big
* @param nativeTokenExtraFee The extra fee to pay in native tokens
* @param data The data to pass to the bridge
*/
function approveAndBridge(IERC20 token, uint256 minAmount, uint256 nativeTokenExtraFee, bytes calldata data)
external
{
// get the balance of the token
uint256 balance = address(token) == NATIVE_TOKEN_ADDRESS
// if native token, reduce the extra fee from balance
// if not enough balance, it will underflow and revert
? address(this).balance - nativeTokenExtraFee
: token.balanceOf(address(this));

// check if the balance is greater than the minAmount
if (balance < minAmount) revert MinAmountNotMet();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think is an issue, but probably we should add in the @dev documentation that minAmount should be an amount not too small if the sell amount is big (basically, the order should have reasonable slippage tolerances). A slippage tolerance of 99.99% could make it almost free for anyone to "consume the hook" making the user's bridge to fail (by a donation of the minAmount).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment: 0422679


// approve the bridgeApprovalTarget if ERC20
if (address(token) != NATIVE_TOKEN_ADDRESS) {
token.forceApprove(bridgeApprovalTarget(), balance);
}

// bridge the token
bridge(token, balance, nativeTokenExtraFee, data);
}

/**
* @dev Returns the address of the contract that should be approved to bridge the token
*/
function bridgeApprovalTarget() public view virtual returns (address);

/**
* @dev Bridges the token
*/
function bridge(IERC20 token, uint256 amount, uint256 nativeTokenExtraFee, bytes calldata data) internal virtual;
}
Loading