Post

Solidity notes

Just a few important terms

Pure and View functions

Pure when no data fetched from the blockchain like adding two numbers and returning it. View used when reading from the blockchain.


Public, external, internal and private

public - all can access

external - Cannot be accessed internally, only externally

internal - only this contract and contracts deriving from it can access

private - can be accessed only from this contract


Receive ether payable

1
2
3
function recEther() public payable {
      toepa[msg.sender] += msg.value;
}

Abi Encode an Decode

1
2
3
function getCalldata() public view returns (bytes memory){
        return abi.encodeWithSignature("getBalance(address)", msg.sender);
}
  • Returns 0xf8b2cb4f0000000000000000000000004b20993bc481177ec7e8f571cecae8a9e22c02db

Response from every hashing function, like keccak-256, are bytes or hex string representing those bytes. Every byte is represented by 2 characters in hex string. So 4 first bytes are 8 first characters. In you example there are

[f8, b2, cb, 4f]

0xf8b2cb4f - Represents “getBalance(address)” called the Method ID

  • When “getBalance(address)” is hashed with with Keccak256 https://emn178.github.io/online-tools/keccak_256.html we get same result.
1
2
3
function decode(bytes calldata payload) public pure returns (address) {
        return abi.decode(payload[4:], (address));
}
  • Returns the address of the msg.sender from earlier.

msg.data = function selector (the function which is called) + function arguments

4 bytes selector + arguments

  • The first four bytes of data are a function selector, where the selector is derived from a hash of the function name (source code) and arguments, truncated to four bytes.

  • What is a Method ID? - Ethereum relies on a unique identifier, known as a Method ID or a function selector, to differentiate between different functions or methods within a smart contract.

Also, you can get the contract ABI from etherscan, and paste it into this tool: https://abi.hashex.org/ which provides then the hashes for your functions. It seems you need to enter at least the first parameter.


Transfer, send, call and sending to a particular function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
contract Rec {

    constructor() payable {}

    function receives() public payable {
        console.log(msg.value);
    }

    receive() external payable { }


}

contract Smend {

    function sendViaTransfer(address payable _to) public payable {
        _to.transfer(0.0001 ether);
    }

    function sendViaSend(address payable _to) public payable {
        bool sent = _to.send(0.0001 ether);
        require(sent, "Failed to send Ether");
    }

    function sendViaCall(address payable _to) public payable {
        (bool sent,) = _to.call{value: 0.0001 ether}("");
        require(sent, "Failed to send Ether");
    }

    function smendTorec(Rec rec) public {
        rec.receives{value: 0.0001 ether}();
    }

    receive() external payable { }

}

Calling a payable function with arguments

1
2
address.function{value:msg.value}(arg1, arg2, arg3)


Delegate call

  • Execute function of another contract like own function of a contract.
  • When contract A executes delegatecall to contract B, B’s code is executed with contract A’s storage, msg.sender and msg.value.

Explained using ethernaut delegate challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

    function pwn() public {
        owner = msg.sender;
    }
}

contract Delegation {
    address public owner;
    Delegate delegate;

    constructor(address _delegateAddress) {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }

    fallback() external {
        (bool result,) = address(delegate).delegatecall(msg.data);
        if (result) {
            this;
        }
    }
}

When delegatecall is invoked the pwn() function is run and it belongs to the Delegation contract so the owner variable is going to be changed for the Delegation contract not for Delegate.

1
2
3
4
5
6
contract Encoder {
    function learnCalldata() public pure  returns (bytes memory){
        return abi.encodeWithSignature("pwn()");
        
    }
}
  • This function returns the abi of the pwn() function which can be passed to the calldata to call the pwn() function.

selfdestruct (depreciated)

  • You can also send balance from one contract to another account by using the SELFDESTRUCT opcode.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force { /*
                   MEOW ?
         /\_/\   /
    ____/ o o \
    /~____  =ø= /
    (______)__m_m)
                   */ }

contract mrAttack {
    function attack(Force forc) payable public {
        address payable addr = payable(address(forc));
        selfdestruct(addr);
    }

    receive() external payable { }
}
  • SELFDESTRUCT opcode most likely going to be deleted in an upcoming fork (https://eips.ethereum.org/EIPS/eip-4758), which would break contracts/protocols relying on it when that happens.

Interface

  • Solidity allows you to interact with other contracts without having their code by using their interface.

Let’s say that we have a simple calculator contract.

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.8.0;

contract Calculator  {
    constructor() {}

    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }

    function subtract(uint a, uint b) public pure returns (uint) {
        return a - b;
    }
}

How to use this contract’s functions in another contract without the source code?, use interface

Here’s our second contract where we want to use the above functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.8.0;

interface CalcInterface {
    function add(uint a, uint b) external pure returns (uint);
    function subtract(uint a, uint b) external pure returns (uint);
}

contract User {
    function add(address _addr, uint a, uint b) public pure returns (uint) {
        CalcInterface calc = CalcInterface(_addr);
        uint m = calc.add(a, b);
        return m;
    }
}

Assembly and extcodesize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
pragma solidity ^0.8.0;
import "forge-std/console.sol";

contract Meow {

    uint256 x;

    function getX() public view returns (uint256) {
        uint256 y ;

        assembly {
            y := sload(0)
        }
        return y;

    }

    function changeX(uint a) public {
        x = a;
    }

    function getCaller() public view returns (address) {
        address _addr;

        assembly {
            _addr := caller()
        }

        return _addr;
    }

    function getCallerSize() public view returns (uint256) {
        uint size;

        assembly {
            size := extcodesize(caller())
        }

        console.log(size);
    }

    

}

contract Caller {
    Meow conc;
    constructor(address _addr) public {
        conc = Meow(_addr);
        conc.getCallerSize();

    }

    function call() public view {
        conc.getCallerSize();
    }
}
  • Caller size is 0 for accounts and >0 for smartcontracts, but when called from the constructor extcodesize returns 0 due to lack of access to code.

Abi.encode , abi.encodepacked, abi.encodewithsignature

➜ uint16 a = 2344 ➜ uint16 b = 359

1
2
3
4
5
6
7
8
9
➜ abi.encode(a,b)
Type: dynamic bytes
├ Hex (Memory):
├─ Length ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000040
├─ Contents ([0x20:..]): 0x00000000000000000000000000000000000000000000000000000000000009280000000000000000000000000000000000000000000000000000000000000167
├ Hex (Tuple Encoded):
├─ Pointer ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000020
├─ Length ([0x20:0x40]): 0x0000000000000000000000000000000000000000000000000000000000000040
└─ Contents ([0x40:..]): 0x00000000000000000000000000000000000000000000000000000000000009280000000000000000000000000000000000000000000000000000000000000167

Abi.encode encodes following the abi specification rules.

1
2
3
4
5
6
7
8
9
➜ abi.encodePacked(a,b)
Type: dynamic bytes
├ Hex (Memory):
├─ Length ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000004
├─ Contents ([0x20:..]): 0x0928016700000000000000000000000000000000000000000000000000000000
├ Hex (Tuple Encoded):
├─ Pointer ([0x00:0x20]): 0x0000000000000000000000000000000000000000000000000000000000000020
├─ Length ([0x20:0x40]): 0x0000000000000000000000000000000000000000000000000000000000000004
└─ Contents ([0x40:..]): 0x0928016700000000000000000000000000000000000000000000000000000000

0x928 = 2344 0x167 = 359

As we can see that it’s being concatenated directly.

This is isn’t safe when generating keccak256 hashes because hash collision can occur.

1
2
3
4
5
6
7
function enc(string memory a, string memory b) public view  {
        bytes32 a  = keccak256(abi.encodePacked(a, b));
        console.logBytes(abi.encodePacked(a,b));

        console.logBytes32(a);

}

a = “AAA” and b = “BBB”

We get the output as

1
2
3
console.log:
0xf6568e65381c5fc6a447ddf0dcb848248282b798b2121d9944a87599b7921358424242
0xf6568e65381c5fc6a447ddf0dcb848248282b798b2121d9944a87599b7921358

Now a = “AA” and b = “ABBB”

1
2
3
console.log:
0xf6568e65381c5fc6a447ddf0dcb848248282b798b2121d9944a87599b792135841424242
0xf6568e65381c5fc6a447ddf0dcb848248282b798b2121d9944a87599b7921358

Still the same output due to the conctenation and packing.

So abi.encode is recommended for hashing since we can get unique inputs for the hash alg.


Inheritence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity "0.8";

contract Pet {
    uint age;
    string name;

    function setNameAge(uint a, string memory b) virtual public {
        age = a;
        name = b;
    }

    function getNameAge() public view returns (uint, string memory) {
        return (age, name);
    }


}

contract Cat is Pet {

    function sayMeow() public pure returns (string memory) {
        return "MEOW";
    }

}

contract Dog is Pet {

    function bark() public pure returns (string memory) {
        return "BARK!";
    }
    
}
  • The contracts cat and dog will inherit the variables and function of the pet contract. The source code of parent will be compiled into the contract.

The functions and variables are independent of a contract , meaning like if I change the age in Cat contract it changes the variable in the cat contract not the parent contract. Just imagine the cat contract having age, name , setName, getNameAge having hidden. They are just there.

override and virtual

If I want to add a function with name setNameAge with a different functionality I should override the present function by adding the override keyword. Also the virtual keyword in the parent function so I can change the function’s functionalities.

Remember that function(a, b) and function(a, b, c) are having two different signatures. So they are like two different functions. so no need to override.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Cat is Pet {

    function sayMeow() public pure virtual returns (string memory) {
        return "MEOW";
    }

}

contract CatSay is Cat {
    function sayMeow() public pure ovverride returns (string memory ) {
        return "MEOWWW";
    }
}


Library

  • State variables can’t be declared nor can send ether.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SPDX-License-Identifier: MIT
pragma solidity 0.8;

library Math {
    function add(uint a, uint b) public pure returns(uint) {
        return a + b;
    }
}


contract Meow {
    function add(uint a , uint b) public pure returns (uint) {
        return Math.add(a, b);
    }
}

Why contract libraries aren’t safe

The delegatecall function changes the variables according to the slot. Below is ethernaut Preservation level.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Preservation {
    // public library contracts
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    uint256 storedTime;
    // Sets the function signature for delegatecall
    bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

    constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
        timeZone1Library = _timeZone1LibraryAddress;
        timeZone2Library = _timeZone2LibraryAddress;
        owner = msg.sender;
    }

    // set the time for timezone 1
    function setFirstTime(uint256 _timeStamp) public {
        timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
    }

    // set the time for timezone 2
    function setSecondTime(uint256 _timeStamp) public {
        timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
    }
}

// Simple library contract to set the time
contract LibraryContract {
    // stores a timestamp
    uint256 storedTime;

    function setTime(uint256 _time) public {
        storedTime = _time;
    }
}

  • Here when stored time is changed using the delegatecall , the value in slot0 is changed for the Preservation contract because storedTime is in slot0. So we can change it to the attackers adresss , and when the setFirstTime function is called again can define a function in the attacker contract to change the third slot which is owner.

and Attacker Contract

1
2
3
4
5
6
7
8
9
10
contract Attacker {
    address public timeZone1Library;
    address public timeZone2Library;
    address public owner;
    
    function setTime(uint256 _time) public {
        owner = 0xe2dd5514FEAFd1925d4e473B8e73E90EcdC103cc;
    }

}

This example demonstrates why the library keyword should be used for building libraries, as it prevents the libraries from storing and accessing state variables. - From ethernaut


Struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
contract Meh {

    struct Person {
        string name;
        uint age;
        uint height;
        uint weight;
    }

    Person[] public people;

    function create(string memory _name, uint _age, uint _height, uint _weight) public {
        Person memory person;
        person.name = _name;
        person.age = _age;
        person.height = _height;
        person.weight = _weight;
        people.push(person);
    }

    function getPerson(uint index) public view returns(string memory, uint, uint, uint) {
        Person memory person = people[index];
        return(person.name, person.age, person.height, person.weight);
    }


}

This post is licensed under CC BY 4.0 by the author.