Эх сурвалжийг харах

Substrate: Implement `delegatecall()` (#1390)

Cyrill Leutwiler 2 жил өмнө
parent
commit
bfa10e260a

+ 33 - 0
docs/language/functions.rst

@@ -308,6 +308,39 @@ calling.
 
 .. _fallback_receive:
 
+Calling an external function using ``delegatecall``
+___________________________________________________
+
+External functions can also be called using ``delegatecall``.
+The difference to a regular ``call`` is that  ``delegatecall`` executes the callee code in the context of the caller:
+
+* The callee will read from and write to the `caller` storage.
+* ``value`` can't be specified for ``delegatecall``; instead it will always stay the same in the callee.
+* ``msg.sender`` does not change; it stays the same as in the callee.
+
+Refer to the `contracts pallet <https://docs.rs/pallet-contracts/latest/pallet_contracts/api_doc/trait.Version0.html#tymethod.delegate_call>`_ 
+and `Ethereum Solidity <https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#delegatecall-and-libraries>`_
+documentations for more information.
+
+``delegatecall`` is commonly used to implement re-usable libraries and 
+`upgradeable contracts <https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable>`_.
+
+.. code-block:: solidity
+
+    function delegate(
+    	address callee,
+    	bytes input
+    ) public returns(bytes result) {
+        (bool ok, result) = callee.delegatecall(input);
+        require(ok);
+    }
+
+..  note::
+    ``delegatecall`` is not available on Solana.
+
+..  note::
+    On Substrate, specifying ``gas`` won't have any effect on ``delegatecall``.
+
 fallback() and receive() function
 _________________________________
 

+ 4 - 4
docs/language/interface_libraries.rst

@@ -36,10 +36,10 @@ When writing libraries there are restrictions compared to contracts:
 
 .. note::
 
-    When using the Ethereum Foundation Solidity compiler, library are a special contract type and libraries are
-    called using `delegatecall`. Parity Substrate has no ``delegatecall`` functionality so Solang statically
-    links the library calls into your contract code. This does make for larger contract code, however this
-    reduces the call overhead and make it possible to do compiler optimizations across library and contract code.
+    When using the Ethereum Foundation Solidity compiler, libraries are a special contract type and are
+    called using `delegatecall`. Solang statically links the library calls into your contract code.
+    This generates larger contract code, however it reduces the call overhead and make it possible to do
+    compiler optimizations across library and contract code.
 
 Library Using For
 _________________

+ 147 - 0
integration/substrate/UpgradeableProxy.sol

@@ -0,0 +1,147 @@
+// Integration test against the delegatecall() function in combination with input forwarding and tail call flags.
+// WARNING: This code is neither EIP compliant nor secure nor audited nor intended to be used in production.
+
+// SPDX-License-Identifier: MIT
+// OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol)
+
+/**
+ * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM
+ * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to
+ * be specified by overriding the virtual {_implementation} function.
+ *
+ * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a
+ * different contract through the {_delegate} function.
+ *
+ * The success and return data of the delegated call will be returned back to the caller of the proxy.
+ */
+abstract contract Proxy {
+    uint32 constant FORWARD_INPUT = 1;
+    uint32 constant TAIL_CALL = 4;
+
+    /**
+     * @dev Delegates the current call to `implementation`.
+     *
+     * This function does not return to its internal call site. It will return directly to the external caller.
+     */
+    function _delegate(address implementation) internal virtual {
+        implementation.delegatecall{flags: FORWARD_INPUT | TAIL_CALL}(hex"");
+    }
+
+    /**
+     * @dev This is a virtual function that should be overridden so it returns the address to which the fallback function
+     * and {_fallback} should delegate.
+     */
+    function _implementation() internal view virtual returns (address);
+
+    /**
+     * @dev Delegates the current call to the address returned by `_implementation()`.
+     *
+     * This function does not return to its internal call site. It will return directly to the external caller.
+     */
+    function _fallback() internal virtual {
+        _beforeFallback();
+        _delegate(_implementation());
+    }
+
+    /**
+     * @dev Fallback function that delegates calls to the address returned by `_implementation()`. It will run if no other
+     * function in the contract matches the call data.
+     */
+    fallback() external virtual {
+        _fallback();
+    }
+
+    /**
+     * @dev Fallback function that delegates calls to the address returned by `_implementation()`. It will run if call data
+     * is empty.
+     */
+    receive() external payable virtual {
+        _fallback();
+    }
+
+    /**
+     * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback`
+     * call, or as part of the Solidity `fallback` or `receive` functions.
+     *
+     * If overridden should call `super._beforeFallback()`.
+     */
+    function _beforeFallback() internal virtual {}
+}
+
+// FIXME: This is NOT EIP-1967.
+// Have to mock it this way until issues #1387 and #1388 are resolved.
+abstract contract StorageSlot {
+    mapping(bytes32 => address) getAddressSlot;
+}
+
+// Minimal proxy implementation; without security
+contract UpgradeableProxy is Proxy, StorageSlot {
+    event Upgraded(address indexed implementation);
+
+    bytes32 internal constant IMPLEMENTATION_SLOT =
+        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+
+    function _setImplementation(address newImplementation) private {
+        // FIXME once issue #809 (supporting address.code) is solved
+        // if (newImplementation.code.length == 0) {
+        //     revert ERC1967InvalidImplementation(newImplementation);
+        // }
+        // FIXME see #1387 and #1388
+        getAddressSlot[IMPLEMENTATION_SLOT] = newImplementation;
+    }
+
+    function upgradeTo(address newImplementation) public {
+        _setImplementation(newImplementation);
+        emit Upgraded(newImplementation);
+    }
+
+    function upgradeToAndCall(
+        address newImplementation,
+        bytes memory data
+    ) public returns (bytes ret) {
+        upgradeTo(newImplementation);
+        (bool ok, ret) = newImplementation.delegatecall(data);
+        require(ok);
+    }
+
+    function _implementation()
+        internal
+        view
+        virtual
+        override
+        returns (address)
+    {
+        return getAddressSlot[IMPLEMENTATION_SLOT];
+    }
+}
+
+// Proxy implementation v1
+contract UpgradeableImplV1 {
+    uint public count;
+
+    constructor() {
+        count = 1;
+    }
+
+    function inc() external {
+        count += 1;
+    }
+}
+
+// Proxy implementation v2
+contract UpgradeableImplV2 {
+    uint public count;
+    string public version;
+
+    constructor() {
+        version = "v2";
+    }
+
+    function inc() external {
+        count += 1;
+    }
+
+    function dec() external {
+        count -= 1;
+    }
+}

+ 33 - 0
integration/substrate/delegate_call.sol

@@ -0,0 +1,33 @@
+// https://solidity-by-example.org/delegatecall/
+
+// SPDX-License-Identifier: MIT
+// pragma solidity ^0.8.17;
+
+// NOTE: Deploy this contract first
+contract Delegatee {
+    // NOTE: storage layout must be the same as contract Delegator
+    uint public num;
+    address public sender;
+    uint public value;
+
+    function setVars(uint _num) public payable {
+        num = _num;
+        sender = msg.sender;
+        value = msg.value;
+    }
+}
+
+contract Delegator {
+    uint public num;
+    address public sender;
+    uint public value;
+
+    function setVars(address _contract, uint _num) public payable {
+        // Delegatee's storage is set, Delegator is not modified.
+        (bool success, bytes memory data) = _contract.delegatecall(
+            abi.encodeWithSignature("setVars(uint256)", _num)
+        );
+        require(success);
+        require(data.length == 0);
+    }
+}

+ 59 - 0
integration/substrate/delegate_call.spec.ts

@@ -0,0 +1,59 @@
+import expect from 'expect';
+import { weight, createConnection, deploy, transaction, aliceKeypair, daveKeypair, debug_buffer, query, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+
+
+describe('Deploy the delegator and the delegatee contracts; test the delegatecall to work correct', () => {
+    let conn: ApiPromise;
+    let delegatee: ContractPromise;
+    let delegator: ContractPromise;
+    let alice: KeyringPair;
+    let dave: KeyringPair;
+
+    before(async function () {
+        alice = aliceKeypair();
+        dave = daveKeypair();
+        conn = await createConnection();
+
+        const delegator_contract = await deploy(conn, alice, 'Delegator.contract', 0n);
+        delegator = new ContractPromise(conn, delegator_contract.abi, delegator_contract.address);
+
+        const delegatee_contract = await deploy(conn, alice, 'Delegatee.contract', 0n);
+        delegatee = new ContractPromise(conn, delegatee_contract.abi, delegatee_contract.address);
+
+        // Set delegatee storage to default values and alice address
+        const gasLimit = await weight(conn, delegatee, 'setVars', [0n]);
+        await transaction(delegatee.tx.setVars({ gasLimit }, [0n]), alice);
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('Executes the delegatee in the context of the delegator', async function () {
+        const value = 1000000n;
+        const arg = 123456789n;
+        const parameters = [delegatee.address, arg];
+
+        const gasLimit = await weight(conn, delegator, 'setVars', parameters);
+        await transaction(delegator.tx.setVars({ gasLimit, value }, ...parameters), dave);
+
+        // Storage of the delegatee must not change
+        let num = await query(conn, alice, delegatee, "num");
+        expect(BigInt(num.output?.toString() ?? "")).toStrictEqual(0n);
+        let balance = await query(conn, alice, delegatee, "value");
+        expect(BigInt(balance.output?.toString() ?? "")).toStrictEqual(0n);
+        let sender = await query(conn, alice, delegatee, "sender");
+        expect(sender.output?.toJSON()).toStrictEqual(alice.address);
+
+        // Storage of the delegator must have changed
+        num = await query(conn, alice, delegator, "num");
+        expect(BigInt(num.output?.toString() ?? "")).toStrictEqual(arg);
+        balance = await query(conn, alice, delegator, "value");
+        expect(BigInt(balance.output?.toString() ?? "")).toStrictEqual(value);
+        sender = await query(conn, alice, delegator, "sender");
+        expect(sender.output?.toJSON()).toStrictEqual(dave.address);
+    });
+});

+ 71 - 0
integration/substrate/upgradeable_proxy.spec.ts

@@ -0,0 +1,71 @@
+import expect from 'expect';
+import { weight, createConnection, deploy, transaction, aliceKeypair, query, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+import { KeyringPair } from '@polkadot/keyring/types';
+import { DecodedEvent } from '@polkadot/api-contract/types';
+import { AccountId, ContractSelector } from '@polkadot/types/interfaces';
+
+describe('Deploy the upgradable proxy and implementations; expect the upgrade mechanism to work', () => {
+    // Helper: Upgrade implementation and execute a constructor that takes no arguments
+    async function upgrade_and_constructor(impl: AccountId, constructor: ContractSelector) {
+        const params = [impl, constructor];
+        const gasLimit = await weight(conn, proxy, 'upgradeToAndCall', params);
+        let result: any = await transaction(proxy.tx.upgradeToAndCall({ gasLimit }, ...params), aliceKeypair());
+
+        let events: DecodedEvent[] = result.contractEvents;
+        expect(events.length).toEqual(1);
+        expect(events[0].event.identifier).toBe("Upgraded");
+        expect(events[0].args.map(a => a.toJSON())[0]).toEqual(params[0].toJSON());
+    }
+
+    let conn: ApiPromise;
+    let alice: KeyringPair;
+    let proxy: ContractPromise;
+    let counter: ContractPromise;
+
+    before(async function () {
+        alice = aliceKeypair();
+        conn = await createConnection();
+
+        const proxy_deployment = await deploy(conn, alice, 'UpgradeableProxy.contract', 0n);
+        proxy = new ContractPromise(conn, proxy_deployment.abi, proxy_deployment.address);
+
+        // Pretend the proxy contract to be implementation V1
+        const implV1 = await deploy(conn, alice, 'UpgradeableImplV1.contract', 0n);
+        await upgrade_and_constructor(implV1.address, implV1.abi.constructors[0].selector);
+        counter = new ContractPromise(conn, implV1.abi, proxy_deployment.address);
+        const count = await query(conn, alice, counter, "count");
+        expect(BigInt(count.output?.toString() ?? "")).toStrictEqual(1n);
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('Tests implementation and upgrading', async function () {
+        // Test implementation V1
+        let gasLimit = await weight(conn, counter, 'inc', []);
+        await transaction(counter.tx.inc({ gasLimit }), alice);
+        await transaction(counter.tx.inc({ gasLimit }), alice);
+        let count = await query(conn, alice, counter, "count");
+        expect(BigInt(count.output?.toString() ?? "")).toStrictEqual(3n);
+
+        // Upgrade to implementation V2
+        const implV2 = await deploy(conn, alice, 'UpgradeableImplV2.contract', 0n);
+        await upgrade_and_constructor(implV2.address, implV2.abi.constructors[0].selector);
+        counter = new ContractPromise(conn, implV2.abi, proxy.address);
+
+        // Test implementation V2
+        count = await query(conn, alice, counter, "count");
+        expect(BigInt(count.output?.toString() ?? "")).toStrictEqual(3n);
+
+        gasLimit = await weight(conn, counter, 'dec', []);
+        await transaction(counter.tx.dec({ gasLimit }), alice);
+        count = await query(conn, alice, counter, "count");
+        expect(BigInt(count.output?.toString() ?? "")).toStrictEqual(2n);
+
+        const version = await query(conn, alice, counter, "version");
+        expect(version.output?.toString()).toStrictEqual("v2");
+    });
+});

+ 13 - 0
src/emit/substrate/mod.rs

@@ -163,6 +163,8 @@ impl SubstrateTarget {
             "debug_message",
             "instantiate",
             "seal_call",
+            "delegate_call",
+            "code_hash",
             "value_transferred",
             "minimum_balance",
             "weight_to_fee",
@@ -294,6 +296,17 @@ impl SubstrateTarget {
             u8_ptr,
             u32_ptr
         );
+        external!(
+            "delegate_call",
+            i32_type,
+            u32_val,
+            u8_ptr,
+            u8_ptr,
+            u32_val,
+            u8_ptr,
+            u32_ptr
+        );
+        external!("code_hash", i32_type, u8_ptr, u8_ptr, u32_ptr);
         external!("transfer", i32_type, u8_ptr, u32_val, u8_ptr, u32_val);
         external!("value_transferred", void_type, u8_ptr, u32_ptr);
         external!("address", void_type, u8_ptr, u32_ptr);

+ 107 - 28
src/emit/substrate/target.rs

@@ -967,46 +967,125 @@ impl<'a> TargetRuntime<'a> for SubstrateTarget {
         payload_len: IntValue<'b>,
         address: Option<PointerValue<'b>>,
         contract_args: ContractArgs<'b>,
-        _ty: ast::CallTy,
+        call_type: ast::CallTy,
         ns: &ast::Namespace,
         loc: Loc,
     ) {
         emit_context!(binary);
 
-        // balance is a u128
-        let value_ptr = binary
-            .builder
-            .build_alloca(binary.value_type(ns), "balance");
-        binary
-            .builder
-            .build_store(value_ptr, contract_args.value.unwrap());
-
         let (scratch_buf, scratch_len) = scratch_buf!();
-
         binary
             .builder
             .build_store(scratch_len, i32_const!(SCRATCH_SIZE as u64));
 
         // do the actual call
-        let ret = call!(
-            "seal_call",
-            &[
-                contract_args.flags.unwrap_or(i32_zero!()).into(),
-                address.unwrap().into(),
-                contract_args.gas.unwrap().into(),
-                value_ptr.into(),
-                payload.into(),
-                payload_len.into(),
-                scratch_buf.into(),
-                scratch_len.into(),
-            ]
-        )
-        .try_as_basic_value()
-        .left()
-        .unwrap()
-        .into_int_value();
+        let ret = match call_type {
+            ast::CallTy::Regular => {
+                let value_ptr = binary
+                    .builder
+                    .build_alloca(binary.value_type(ns), "balance");
+                binary
+                    .builder
+                    .build_store(value_ptr, contract_args.value.unwrap());
+                let ret = call!(
+                    "seal_call",
+                    &[
+                        contract_args.flags.unwrap_or(i32_zero!()).into(),
+                        address.unwrap().into(),
+                        contract_args.gas.unwrap().into(),
+                        value_ptr.into(),
+                        payload.into(),
+                        payload_len.into(),
+                        scratch_buf.into(),
+                        scratch_len.into(),
+                    ]
+                )
+                .try_as_basic_value()
+                .left()
+                .unwrap()
+                .into_int_value();
+                log_return_code(binary, "seal_call", ret);
+                ret
+            }
+            ast::CallTy::Delegate => {
+                // delegate_call asks for a code hash instead of an address
+                let hash_len = i32_const!(32); // FIXME: This is configurable like the address length
+                let code_hash_out_ptr = binary.builder.build_array_alloca(
+                    binary.context.i8_type(),
+                    hash_len,
+                    "code_hash_out_ptr",
+                );
+                let code_hash_out_len_ptr = binary
+                    .builder
+                    .build_alloca(binary.context.i32_type(), "code_hash_out_len_ptr");
+                binary.builder.build_store(code_hash_out_len_ptr, hash_len);
+                let code_hash_ret = call!(
+                    "code_hash",
+                    &[
+                        address.unwrap().into(),
+                        code_hash_out_ptr.into(),
+                        code_hash_out_len_ptr.into(),
+                    ]
+                )
+                .try_as_basic_value()
+                .left()
+                .unwrap()
+                .into_int_value();
+                log_return_code(binary, "seal_code_hash", code_hash_ret);
+
+                let code_hash_found = binary.builder.build_int_compare(
+                    IntPredicate::EQ,
+                    code_hash_ret,
+                    i32_zero!(),
+                    "code_hash_found",
+                );
+                let entry = binary.builder.get_insert_block().unwrap();
+                let call_block = binary
+                    .context
+                    .append_basic_block(function, "code_hash_found");
+                let not_found_block = binary
+                    .context
+                    .append_basic_block(function, "code_hash_not_found");
+                let done_block = binary.context.append_basic_block(function, "done_block");
+                binary.builder.build_conditional_branch(
+                    code_hash_found,
+                    call_block,
+                    not_found_block,
+                );
 
-        log_return_code(binary, "seal_call", ret);
+                binary.builder.position_at_end(not_found_block);
+                let msg = "delegatecall callee is not a contract account";
+                self.log_runtime_error(binary, msg.into(), Some(loc), ns);
+                binary.builder.build_unconditional_branch(done_block);
+
+                binary.builder.position_at_end(call_block);
+                let delegate_call_ret = call!(
+                    "delegate_call",
+                    &[
+                        contract_args.flags.unwrap_or(i32_zero!()).into(),
+                        code_hash_out_ptr.into(),
+                        payload.into(),
+                        payload_len.into(),
+                        scratch_buf.into(),
+                        scratch_len.into(),
+                    ]
+                )
+                .try_as_basic_value()
+                .left()
+                .unwrap()
+                .into_int_value();
+                log_return_code(binary, "seal_delegate_call", delegate_call_ret);
+                binary.builder.build_unconditional_branch(done_block);
+
+                binary.builder.position_at_end(done_block);
+                let ty = binary.context.i32_type();
+                let ret = binary.builder.build_phi(ty, "storage_res");
+                ret.add_incoming(&[(&code_hash_ret, not_found_block), (&ty.const_zero(), entry)]);
+                ret.add_incoming(&[(&delegate_call_ret, call_block), (&ty.const_zero(), entry)]);
+                ret.as_basic_value().into_int_value()
+            }
+            ast::CallTy::Static => unreachable!("sema does not allow this"),
+        };
 
         let is_success =
             binary

+ 8 - 1
src/sema/expression/function_call.rs

@@ -1281,7 +1281,7 @@ fn try_type_method(
 
             let ty = match func.name.as_str() {
                 "call" => Some(CallTy::Regular),
-                "delegatecall" if ns.target == Target::EVM => Some(CallTy::Delegate),
+                "delegatecall" if ns.target != Target::Solana => Some(CallTy::Delegate),
                 "staticcall" if ns.target == Target::EVM => Some(CallTy::Static),
                 _ => None,
             };
@@ -1299,6 +1299,13 @@ fn try_type_method(
                     return Err(());
                 }
 
+                if ty == CallTy::Delegate && ns.target.is_substrate() && call_args.gas.is_some() {
+                    diagnostics.push(Diagnostic::warning(
+                        *loc,
+                        "'gas' specified on 'delegatecall' will be ignored".into(),
+                    ));
+                }
+
                 if args.len() != 1 {
                     diagnostics.push(Diagnostic::error(
                         *loc,

+ 16 - 0
tests/codegen_testcases/solidity/substrate_delegate_call.sol

@@ -0,0 +1,16 @@
+// RUN: --target substrate --emit cfg
+
+contract CallFlags {
+    function delegate_call(address _address, uint32 _flags) public returns (bytes ret) {
+        (bool ok, ret) = _address.delegatecall{flags: _flags}(hex"deadbeef");
+        (ok, ret) = address(this).delegatecall(hex"cafebabe");
+        // CHECK: block0: # entry
+        // CHECK: ty:address %_address = (arg #0)
+        // CHECK: ty:uint32 %_flags = (arg #1)
+        // CHECK: ty:bytes %ret = (alloc bytes len uint32 0)
+        // CHECK: %success.temp.4 = external call::delegate address:(arg #0) payload:(alloc bytes uint32 4 hex"deadbeef") value:uint128 0 gas:uint64 0 accounts: seeds: contract|function:_ flags:(arg #1)
+        // CHECK: ty:bytes %ret = (external call return data)
+        // CHECK: %success.temp.5 = external call::delegate address:address((load (builtin GetAddress ()))) payload:(alloc bytes uint32 4 hex"cafebabe") value:uint128 0 gas:uint64 0 accounts: seeds: contract|function:_ flags:
+        // CHECK: ty:bytes %ret = (external call return data)
+    }
+}

+ 6 - 8
tests/contract_testcases/substrate/builtins/call.sol

@@ -1,10 +1,8 @@
+contract main {
+    function test() public {
+        address x = address(0);
 
-        contract main {
-            function test() public {
-                address x = address(0);
-
-                x.delegatecall(hex"1222");
-            }
-        }
+        x.delegatecall(hex"1222");
+    }
+}
 // ---- Expect: diagnostics ----
-// error: 6:19-31: method 'delegatecall' does not exist

+ 12 - 0
tests/contract_testcases/substrate/calls/delegate_call.sol

@@ -0,0 +1,12 @@
+contract Delegate {
+    function delegate(
+        address callee,
+        bytes input
+    ) public returns(bytes result) {
+        (bool ok, result) = callee.delegatecall{gas: 123}(input);
+        require(ok);
+    }
+}
+
+// ---- Expect: diagnostics ----
+// warning: 6:29-65: 'gas' specified on 'delegatecall' will be ignored