Skip to content
Merged
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
Contributor 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();

/// @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
Collaborator

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
Contributor 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