Skip to content

Commit

Permalink
Substrate: Implement delegatecall() (hyperledger-solang#1390)
Browse files Browse the repository at this point in the history
  • Loading branch information
xermicus authored Jun 27, 2023
1 parent 153314e commit bfa10e2
Show file tree
Hide file tree
Showing 12 changed files with 509 additions and 41 deletions.
33 changes: 33 additions & 0 deletions docs/language/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
_________________________________

Expand Down
8 changes: 4 additions & 4 deletions docs/language/interface_libraries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
_________________
Expand Down
147 changes: 147 additions & 0 deletions integration/substrate/UpgradeableProxy.sol
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions integration/substrate/delegate_call.sol
Original file line number Diff line number Diff line change
@@ -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 changes: 59 additions & 0 deletions integration/substrate/delegate_call.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions integration/substrate/upgradeable_proxy.spec.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions src/emit/substrate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ impl SubstrateTarget {
"debug_message",
"instantiate",
"seal_call",
"delegate_call",
"code_hash",
"value_transferred",
"minimum_balance",
"weight_to_fee",
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit bfa10e2

Please sign in to comment.