Repository: nomad-xyz/ExcessivelySafeCall
Branch: main
Commit: 81cd99ce3e69
Files: 9
Total size: 16.6 KB
Directory structure:
gitextract_29umko3s/
├── .github/
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── README.md
├── foundry.toml
├── package.json
└── src/
├── ExcessivelySafeCall.sol
└── test/
└── ExcessivelySafeCall.t.sol
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/test.yml
================================================
on: [push]
name: test
jobs:
check:
name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Run tests @ 0.8.13
run: forge test -vvv --use 0.8.13
- name: Run tests @ 0.7.6
run: forge test -vvv --use 0.7.6
- name: Run snapshot
run: forge snapshot
================================================
FILE: .gitignore
================================================
cache/
out/
================================================
FILE: .gitmodules
================================================
[submodule "lib/ds-test"]
path = lib/ds-test
url = https://github.com/dapphub/ds-test
================================================
FILE: CHANGELOG.md
================================================
# Changelog
### Unreleased
### 0.0.1-rc.1
- first release
================================================
FILE: README.md
================================================
# ExcessivelySafeCall
This solidity library helps you call untrusted contracts safely. Specifically,
it seeks to prevent _all possible_ ways that the callee can maliciously cause
the caller to revert. Most of these revert cases are covered by the use of a
[low-level call](https://solidity-by-example.org/call/). The main difference
with between `address.call()`call and `address.excessivelySafeCall()` is that
a regular solidity call will **automatically** copy bytes to memory without
consideration of gas.
This is to say, a low-level solidity call will copy _any amount of bytes_ to
local memory. When bytes are copied from returndata to memory, the
[memory expansion cost
](https://ethereum.stackexchange.com/questions/92546/what-is-expansion-cost) is
paid. This means that when using a standard solidity call, the callee can
**"returnbomb"** the caller, imposing an arbitrary gas cost. Because this gas is
paid _by the caller_ and _in the caller's context_, it can cause the caller to
run out of gas and halt execution.
To prevent returnbombing, we provide `excessivelySafeCall` and
`excessivelySafeStaticCall`. These behave similarly to solidity's low-level
calls, however, they allow the user to specify a maximum number of bytes to be
copied to local memory. E.g. a user desiring a single return value should
specify a `_maxCopy` of 32 bytes. Refusing to copy large blobs to local memory
effectively prevents the callee from triggering local OOG reversion. We _also_ recommend careful consideration of the gas amount passed to untrusted
callees.
Consider the following contracts:
```solidity
contract BadGuy {
function youveActivateMyTrapCard() external pure returns (bytes memory) {
assembly{
revert(0, 1_000_000)
}
}
}
contract Mark {
function oops(address badGuy) {
bool success;
bytes memory ret;
// Mark pays a lot of gas for this copy 😬😬😬
(success, ret) == badGuy.call(
SOME_GAS,
abi.encodeWithSelector(
BadGuy.youveActivateMyTrapCard.selector
)
);
// Mark may OOG here, preventing local state changes
importantCleanup();
}
}
contract ExcessivelySafeSam {
using ExcessivelySafeCall for address;
// Sam is cool and doesn't get returnbombed
function sunglassesEmoji(address badGuy) {
bool success;
bytes memory ret;
(success, ret) == badGuy.excessivelySafeCall(
SOME_GAS,
32, // <-- the magic. Copy no more than 32 bytes to memory
abi.encodeWithSelector(
BadGuy.youveActivateMyTrapCard.selector
)
);
// Sam can afford to clean up after himself.
importantCleanup();
}
}
```
## When would I use this
`ExcessivelySafeCall` prevents malicious callees from affecting post-execution
cleanup (e.g. state-based replay protection). Given that a dev is unlikely to
hard-code a call to a malicious contract, we expect most danger to come from
dynamic dispatch protocols, where neither the callee nor the code being called
is known to the developer ahead of time.
Dynamic dispatch in solidity is probably _most_ useful for metatransaction
protocols. This includes gas-abstraction relayers, smart contract wallets,
bridges, etc.
Nomad uses excessively safe calls for safe processing of cross-domain messages.
This guarantees that a message recipient cannot interfere with safe operation
of the cross-domain communication channel and message processing layer.
## Interacting with the repo
**To install in your project**:
- install [Foundry](https://github.com/gakonst/foundry)
- `forge install nomad-xyz/ExcessivelySafeCall`
**To run tests**:
- install [Foundry](https://github.com/gakonst/foundry)
- `forge test`
## A note on licensing:
Tests are licensed GPLv3, as they extend the `DSTest` contract. Non-test work
is avialable under user's choice of MIT and Apache2.0.
================================================
FILE: foundry.toml
================================================
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
remappings = ['ds-test/=lib/ds-test/src/']
================================================
FILE: package.json
================================================
{
"name": "@nomad-xyz/excessively-safe-call",
"version": "0.0.1-rc.1",
"description": "Helps you call untrusted contracts safely",
"keywords": [
"nomad",
"excessively safe call"
],
"homepage": "https://github.com/nomad-xyz/ExcessivelySafeCall#readme",
"bugs": {
"url": "https://github.com/nomad-xyz/ExcessivelySafeCall/issues"
},
"repository": {
"type": "git",
"url": "git@github.com:nomad-xyz/ExcessivelySafeCall.git"
},
"license": "Apache-2.0 OR MIT",
"author": "Illusory Systems Inc.",
"main": "src/ExcessivelySafeCall.sol",
"files": [
"src/ExcessivelySafeCall.sol"
]
}
================================================
FILE: src/ExcessivelySafeCall.sol
================================================
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.7.6;
library ExcessivelySafeCall {
uint256 constant LOW_28_MASK =
0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
/// @notice Use when you _really_ really _really_ don't trust the called
/// contract. This prevents the called contract from causing reversion of
/// the caller in as many ways as we can.
/// @dev The main difference between this and a solidity low-level call is
/// that we limit the number of bytes that the callee can cause to be
/// copied to caller memory. This prevents stupid things like malicious
/// contracts returning 10,000,000 bytes causing a local OOG when copying
/// to memory.
/// @param _target The address to call
/// @param _gas The amount of gas to forward to the remote contract
/// @param _value The value in wei to send to the remote contract
/// @param _maxCopy The maximum number of bytes of returndata to copy
/// to memory.
/// @param _calldata The data to send to the remote contract
/// @return success and returndata, as `.call()`. Returndata is capped to
/// `_maxCopy` bytes.
function excessivelySafeCall(
address _target,
uint256 _gas,
uint256 _value,
uint16 _maxCopy,
bytes memory _calldata
) internal returns (bool, bytes memory) {
// set up for assembly call
uint256 _toCopy;
bool _success;
bytes memory _returnData = new bytes(_maxCopy);
// dispatch message to recipient
// by assembly calling "handle" function
// we call via assembly to avoid memcopying a very large returndata
// returned by a malicious contract
assembly {
_success := call(
_gas, // gas
_target, // recipient
_value, // ether value
add(_calldata, 0x20), // inloc
mload(_calldata), // inlen
0, // outloc
0 // outlen
)
// limit our copy to 256 bytes
_toCopy := returndatasize()
if gt(_toCopy, _maxCopy) {
_toCopy := _maxCopy
}
// Store the length of the copied bytes
mstore(_returnData, _toCopy)
// copy the bytes from returndata[0:_toCopy]
returndatacopy(add(_returnData, 0x20), 0, _toCopy)
}
return (_success, _returnData);
}
/// @notice Use when you _really_ really _really_ don't trust the called
/// contract. This prevents the called contract from causing reversion of
/// the caller in as many ways as we can.
/// @dev The main difference between this and a solidity low-level call is
/// that we limit the number of bytes that the callee can cause to be
/// copied to caller memory. This prevents stupid things like malicious
/// contracts returning 10,000,000 bytes causing a local OOG when copying
/// to memory.
/// @param _target The address to call
/// @param _gas The amount of gas to forward to the remote contract
/// @param _maxCopy The maximum number of bytes of returndata to copy
/// to memory.
/// @param _calldata The data to send to the remote contract
/// @return success and returndata, as `.call()`. Returndata is capped to
/// `_maxCopy` bytes.
function excessivelySafeStaticCall(
address _target,
uint256 _gas,
uint16 _maxCopy,
bytes memory _calldata
) internal view returns (bool, bytes memory) {
// set up for assembly call
uint256 _toCopy;
bool _success;
bytes memory _returnData = new bytes(_maxCopy);
// dispatch message to recipient
// by assembly calling "handle" function
// we call via assembly to avoid memcopying a very large returndata
// returned by a malicious contract
assembly {
_success := staticcall(
_gas, // gas
_target, // recipient
add(_calldata, 0x20), // inloc
mload(_calldata), // inlen
0, // outloc
0 // outlen
)
// limit our copy to 256 bytes
_toCopy := returndatasize()
if gt(_toCopy, _maxCopy) {
_toCopy := _maxCopy
}
// Store the length of the copied bytes
mstore(_returnData, _toCopy)
// copy the bytes from returndata[0:_toCopy]
returndatacopy(add(_returnData, 0x20), 0, _toCopy)
}
return (_success, _returnData);
}
/**
* @notice Swaps function selectors in encoded contract calls
* @dev Allows reuse of encoded calldata for functions with identical
* argument types but different names. It simply swaps out the first 4 bytes
* for the new selector. This function modifies memory in place, and should
* only be used with caution.
* @param _newSelector The new 4-byte selector
* @param _buf The encoded contract args
*/
function swapSelector(bytes4 _newSelector, bytes memory _buf)
internal
pure
{
require(_buf.length >= 4);
uint256 _mask = LOW_28_MASK;
assembly {
// load the first word of
let _word := mload(add(_buf, 0x20))
// mask out the top 4 bytes
// /x
_word := and(_word, _mask)
_word := or(_newSelector, _word)
mstore(add(_buf, 0x20), _word)
}
}
}
================================================
FILE: src/test/ExcessivelySafeCall.t.sol
================================================
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.7.6;
import "ds-test/test.sol";
import "src/ExcessivelySafeCall.sol";
contract ContractTest is DSTest {
using ExcessivelySafeCall for address;
address target;
CallTarget t;
function returnSize() internal pure returns (uint256 _bytes) {
assembly {
_bytes := returndatasize()
}
}
function setUp() public {
t = new CallTarget();
target = address(t);
}
function testCall() public {
bool _success;
bytes memory _ret;
(_success, _ret) = target.excessivelySafeCall(
100_000,
0,
0,
abi.encodeWithSelector(CallTarget.one.selector)
);
assertTrue(_success);
assertEq(_ret.length, 0);
assertEq(t.called(), 1);
(_success, _ret) = target.excessivelySafeCall(
100_000,
0,
0,
abi.encodeWithSelector(CallTarget.two.selector)
);
assertTrue(_success);
assertEq(_ret.length, 0);
assertEq(t.called(), 2);
(_success, _ret) = target.excessivelySafeCall(
100_000,
0,
0,
abi.encodeWithSelector(CallTarget.any.selector, 5)
);
assertTrue(_success);
assertEq(_ret.length, 0);
assertEq(t.called(), 5);
(_success, _ret) = target.excessivelySafeCall(
100_000,
69,
0,
abi.encodeWithSelector(CallTarget.payme.selector)
);
assertTrue(_success);
assertEq(_ret.length, 0);
assertEq(t.called(), 69);
}
function testStaticCall() public {
bool _success;
bytes memory _ret;
(_success, _ret) = target.excessivelySafeStaticCall(
100_000,
0,
abi.encodeWithSelector(CallTarget.two.selector)
);
assertEq(t.called(), 0, "t modified state");
assertTrue(!_success, "staticcall should error on state modification");
}
function testCopy(uint16 _maxCopy, uint16 _requested) public {
uint16 _toCopy = _maxCopy < _requested ? _maxCopy : _requested;
bool _success;
bytes memory _ret;
(_success, _ret) = target.excessivelySafeCall(
100_000,
0,
_maxCopy,
abi.encodeWithSelector(CallTarget.retBytes.selector, uint256(_requested))
);
assertTrue(_success);
assertEq(_ret.length, _toCopy, "return copied wrong amount");
(_success, _ret) = target.excessivelySafeCall(
100_000,
0,
_maxCopy,
abi.encodeWithSelector(CallTarget.revBytes.selector, uint256(_requested))
);
assertTrue(!_success);
assertEq(_ret.length, _toCopy, "revert copied wrong amount");
}
function testStaticCopy(uint16 _maxCopy, uint16 _requested) public {
uint16 _toCopy = _maxCopy < _requested ? _maxCopy : _requested;
bool _success;
bytes memory _ret;
(_success, _ret) = target.excessivelySafeStaticCall(
100_000,
_maxCopy,
abi.encodeWithSelector(CallTarget.retBytes.selector, uint256(_requested))
);
assertTrue(_success);
assertEq(_ret.length, _toCopy, "return copied wrong amount");
(_success, _ret) = target.excessivelySafeStaticCall(
100_000,
_maxCopy,
abi.encodeWithSelector(CallTarget.revBytes.selector, uint256(_requested))
);
assertTrue(!_success);
assertEq(_ret.length, _toCopy, "revert copied wrong amount");
}
function testBadBehavior() public {
bool _success;
bytes memory _ret;
(_success, _ret) = target.excessivelySafeCall(
3_000_000,
0,
32,
abi.encodeWithSelector(CallTarget.badRet.selector)
);
assertTrue(_success);
assertEq(returnSize(), 1_000_000, "didn't return all");
assertEq(_ret.length, 32, "revert didn't truncate");
(_success, _ret) = target.excessivelySafeCall(
3_000_000,
0,
32,
abi.encodeWithSelector(CallTarget.badRev.selector)
);
assertTrue(!_success);
assertEq(returnSize(), 1_000_000, "didn't return all");
assertEq(_ret.length, 32, "revert didn't truncate");
}
function testStaticBadBehavior() public {
bool _success;
bytes memory _ret;
(_success, _ret) = target.excessivelySafeStaticCall(
2_002_000,
32,
abi.encodeWithSelector(CallTarget.badRet.selector)
);
assertTrue(_success);
assertEq(returnSize(), 1_000_000, "didn't return all");
assertEq(_ret.length, 32, "revert didn't truncate");
(_success, _ret) = target.excessivelySafeStaticCall(
2_002_000,
32,
abi.encodeWithSelector(CallTarget.badRev.selector)
);
assertTrue(!_success);
assertEq(returnSize(), 1_000_000, "didn't return all");
assertEq(_ret.length, 32, "revert didn't truncate");
}
}
contract CallTarget {
uint256 public called;
constructor () {}
function one() external {
called = 1;
}
function two() external {
called = 2;
}
function any(uint256 _num) external {
called = _num;
}
function payme() external payable {
called = msg.value;
}
function retBytes(uint256 _bytes) public pure {
assembly {
return(0, _bytes)
}
}
function revBytes(uint256 _bytes) public pure {
assembly {
revert(0, _bytes)
}
}
function badRet() external pure returns (bytes memory) {
retBytes(1_000_000);
}
function badRev() external pure {
revBytes(1_000_000);
}
}
gitextract_29umko3s/
├── .github/
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── README.md
├── foundry.toml
├── package.json
└── src/
├── ExcessivelySafeCall.sol
└── test/
└── ExcessivelySafeCall.t.sol
Condensed preview — 9 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (18K chars).
[
{
"path": ".github/workflows/test.yml",
"chars": 507,
"preview": "on: [push]\n\nname: test\n\njobs:\n check:\n name: Foundry project\n runs-on: ubuntu-latest\n steps:\n - uses: act"
},
{
"path": ".gitignore",
"chars": 12,
"preview": "cache/\nout/\n"
},
{
"path": ".gitmodules",
"chars": 88,
"preview": "[submodule \"lib/ds-test\"]\n\tpath = lib/ds-test\n\turl = https://github.com/dapphub/ds-test\n"
},
{
"path": "CHANGELOG.md",
"chars": 61,
"preview": "# Changelog\n\n### Unreleased\n\n### 0.0.1-rc.1\n\n- first release\n"
},
{
"path": "README.md",
"chars": 3973,
"preview": "# ExcessivelySafeCall\n\nThis solidity library helps you call untrusted contracts safely. Specifically,\nit seeks to preven"
},
{
"path": "foundry.toml",
"chars": 100,
"preview": "[profile.default]\nsrc = 'src'\nout = 'out'\nlibs = ['lib']\nremappings = ['ds-test/=lib/ds-test/src/']\n"
},
{
"path": "package.json",
"chars": 626,
"preview": "{\n \"name\": \"@nomad-xyz/excessively-safe-call\",\n \"version\": \"0.0.1-rc.1\",\n \"description\": \"Helps you call untrusted co"
},
{
"path": "src/ExcessivelySafeCall.sol",
"chars": 5605,
"preview": "// SPDX-License-Identifier: MIT OR Apache-2.0\npragma solidity >=0.7.6;\n\nlibrary ExcessivelySafeCall {\n uint256 consta"
},
{
"path": "src/test/ExcessivelySafeCall.t.sol",
"chars": 6010,
"preview": "// SPDX-License-Identifier: GPL-3.0-or-later\npragma solidity >=0.7.6;\n\nimport \"ds-test/test.sol\";\nimport \"src/Excessivel"
}
]
About this extraction
This page contains the full source code of the nomad-xyz/ExcessivelySafeCall GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 9 files (16.6 KB), approximately 4.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.