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); } }