# call vs delegatecall vs staticcall
Khi lập trình $smart contract$ trên **Ethereum** nói riêng và các nền tảng khác nói chung, đặc biệt là ở các dự án phức tạp và đặc thù thì việc ***call*** $contract$ từ $contract$ khác là rất hay được sử dụng. Lập trình $smart contract$ hiện nay hầu hết đều liên quan đến số tiền không nhỏ nên chỉ cần một lỗi làm bé tí ti cũng khiến chúng ta phải trả giá rất nặng nề, đơn vị được tính bằng nhiều triệu đô. Các bạn có thể tìm hiểu thêm về các vụ hack hàng chục triệu đô trong vài năm gần đây. Một số quan điểm cho rằng các vụ tấn công này một phần là do lỗi thiết kế hệ thống, cho phép lập trình viên thực hiện những thao tác dẫn đến nhiều lỗ hổng và lỗi logic không đáng có. Tuy nhiên, điều gì cũng có hai mặt của nó, việc này lại cho phép lập trình viên linh động và sáng tạo nhiều hơn. Chính vì vậy, ở phía các lập trình viên thì điều chúng ta có thể làm tốt nhất là hiểu rõ công cụ chúng ta sử dụng để hạn chế tối đa những lỗi nghiêm trọng có thể xảy ra. Giống như khi khi chụp ảnh vậy, khi bạn đã thành thạo rồi thì bạn mong muốn được điều chỉnh các thông số tuỳ ý chứ không bị gò bó và giới hạn bởi các thông số cố định hay đã được cài đặt sẵn.
Lập trình $smart contract$ trên máy ảo Ethereum--**EVM**-- cho phép lập trình viên sử dụng *assembly* để có thể can thiệp ở mức sâu hơn mà đôi khi sử dụng **solidity** đơn thuần không thực hiện được hoặc một số tính toán có thể tối ưu lượng *gas* cần sử dụng để giảm chi phí giao dịch. Và nó cũng là con dao hai lưỡi nếu chúng ta sử dụng mà không nắm rõ về nó.
Quay lại về việc gọi qua lại giữa các smart contract khác nhau, cả 3 cách gọi call, delegatecall và static call đều có thể thực hiện bằng solidity hoặc assembly; tuỳ vào tình huống mà chúng ta sử dụng cách thức hợp lý hơn. Mục đích của bài viết này là phân biệt sự khác nhau và giống nhau giữa 3 cách gọi đã kể ở trên.
## call
Thông thường, nếu chúng ta import một contract này vào source code của contract khác thì việc call trờ nên rất tự nhiên như chúng ta vẫn làm trong các ngôn ngữ lập trình truyền thống khác.
Một vi dụ đơn giản như sau, chúng ta có một contract để lưu giữ thông tin
```solidity=
pragma solidity ^0.5.8;
contract Storage {
uint public val;
constructor(uint v) public {
val = v;
}
function setValue(uint v) public {
val = v;
}
}
```
Chúng ta muốn deploy một contract Machine sẽ gọi đến contract Storage.
```solidity=
pragma solidity ^0.5.8;
import "./Storage.sol";
contract Machine {
Storage public s;
constructor(Storage addr) public {
s = addr;
calculateResult = 0;
}
function saveValue(uint x) public returns (bool) {
s.setValue(x);
return true;
}
function getValue() public view returns (uint) {
return s.val();
}
}
```
Dễ nhận thấy rằng, ở dòng số #3 `import "./Storage.sol";` , contract Storage được import trực tiếp vào contract Machine và khi deploy contract Machine thì contract Storage cũng được deploy cùng luôn, contract Machine tham chiếu trực tiếp đến contract Storage nên việc call các method của contract Storage từ contract Machine được thực hiện đơn giản như ở dòng #14 `s.setValue(x);` và giá trị được thay đổi cho dù được truyền vào qua tham số và được thực thi ở contract Machine nhưng trạng thái thay đổi được phản ánh vào contract Storage.

Tuy nhiên, nếu trong trường hợp để tiết kiệm chi phí hoặc có thể do một số yêu cầu đặc biệt của dư án thì contract Storage có thể được deploy một cách độc lập với contract Machine. Tuỳ vào từng trường hợp thì chúng ta có thể lựa chọn 1 trong 2 cách sau đây:
1. Sử dụng interface
2. Sử dụng hàm call
### Sử dụng interface
Interface của solidity không khác gì interface ở các ngôn ngữ lập trình phổ biến nên cũng không cần thiết phải gỉải thích lại ở bài viết này. Bằng cách sử dụng interface, chúng ta vẫn có thể call các method của contract Storage mà không cần import code của nó vào contract Machine. Cụ thể cách thức implement cho contract Machine có thể như sau:
```solidity=
pragma solidity ^0.5.8;
interface StorageInterface {
function saveValue(uint x) public returns (bool);
}
contract Machine {
StorageInterface public s;
constructor(StorageInterface addr) public {
s = addr;
calculateResult = 0;
}
function saveValue(uint x) public returns (bool) {
s.setValue(x);
return true;
}
function getValue() public view returns (uint) {
return s.val();
}
}
```
### Sử dụng hàm **call**
Không phải lúc nào chúng ta cũng có thể sử dụng `interface` vì trong nhiều trường hợp, chúng ta không biết trước được ABI của contract, chúng ta chỉ có thông tin này lúc runtime thì gần như bắt buộc chúng ta phải sử dụng đến hàm **call** để gọi đến method của một smart contract độc lập khác.
Thực chất thì EVM kích hoạt một lời gọi hàm của smart contract qua một chuỗi byte; trong đó, 4 byte đầu tiên của hash từ giá trị của method signature, tiếp sau đó mà các chuỗi có độ dài cố định là 32 bytes. Chúng ta có thể thấy rõ khi xem chi tiết giao dịch trên [etherscan](https://etherscan.io/)

`MethodID: 0x2d2da806` chính là 4 bytes đầu tiên của giá trị hash keccak-256 của method signature `depositETH(address)`. Nếu muốn bạn có thể kiểm tra bằng cách sử dụng [công cụ hash keccak-256 online](https://emn178.github.io/online-tools/keccak_256.html)

`[0]: 000000000000000000000000e105ba42b66d08ac7ca7fc48c583599044a6dab3` giá trị tiếp sau đó là input data của method dưới dạng chuỗi các số 32 bytes và được đánh số từ 0.
Chính vì vậy mà khi sử dụng hàm call chúng ta cũng phải tính toán **MethodID** theo đúng mô tả ở trên mà truyền vào tham số tương ứng của hàm call để có thể gọi được method mong muốn của smart contract nào đó. Quay lại ví dụ ở trên thì chúng ta có thể cài đặt contract Machine như sau
```solidity=
pragma solidity ^8.0.0;
contract Machine {
address immutable public s;
constructor(address addr) public {
s = addr;
calculateResult = 0;
}
function saveValue(uint x) public returns (bool) {
bool success;
(successs, _) = s.call(abi.encodeWithSignature("setValue(uint)", x));
if (!successs) {
// solhint-disable-next-line no-inline-assembly
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
return true;
}
}
```
## delegatecall
Khi sử dụng $call$ thì `msg.sender` không phải là `sender` ban đầu mà khi đó `sender` được thay đổi thành caller contract. Như ở ví dụ trên thì `sender` là contract Machine và state được thay đổi nằm ở context của contract Storage. Nhưng khi sử dụng $delegatecall$ thì `msg.sender` được giữ nguyên như ban đầu không thay đổi và state thay đổi ở context của Manchine, chỉ mượn code của Storage rồi thực thi trên context của contract Machine.

$delegatecall$ hay được sử dụng trong proxy pattern, khi mà implement logic code chung ở một contract độc lập và contract để lưu state của mỗi người dùng thì được lưu riêng biệt ở contract proxy. Và có thể thay đổi hay upgrade logic code bằng cách thay đổi logic code của contract độc lập và trỏ lại implement contract ở contract proxy.
Ví dụ một proxy contract có thể được cài đặt sử dụng $delegatecall$ ở dòng thứ 18 như sau
```solidity=
pragma solidity ^0.8.3;
contract Proxy {
address immutable public implementation;
event Received(uint indexed value, address indexed sender, bytes data);
constructor(address _implementation) {
implementation = _implementation;
}
fallback() external payable {
address target = implementation;
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), target, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {revert(0, returndatasize())}
default {return (0, returndatasize())}
}
}
receive() external payable {
emit Received(msg.value, msg.sender, "");
}
}
```
## staticcall
$staticcall$ thì cơ bản giống với $call$ chỉ khác là $staticcall$ không cho phép thay đổi state của smart contract.
> [name=Ha ĐANG ]
> [time=Sun, Jun 20, 2021 2:01 AM]