智能合约语法笔记

区块链语法笔记

demo 铸币代码

pragma solidity ^0.4;
contract Coin{
    //set the "address" type variable minter
    address public minter; 
    /*convert "address"(for storing address or key ) 
    to the type of "uint" which is as subscrip of object balances*/
    mapping (address =>uint) public balances; 
    // set an event so as to be seen publicly
    event Sent(address from,address to,uint amount); 
    //constructor only run once when creating contract,unable to invoke
    //"msg" is the address of creator."msg.sender"  is 
    constructor()public{
        minter=msg.sender;
    }
    //铸币
    //can only be called by creator
    function mint(address receiver,uint amount)public{
        require(msg.sender ==minter);
        balances[receiver]+=amount;
    }
    //转账 
    function send(address receiver,uint amount)public{
        require(balances[msg.sender]>= amount);
        balances[msg.sender]-=amount;
        balances[receiver]+=amount;
        emit Sent(msg.sender,receiver,amount);
    }

}

源文件结构

pragma 版本标识

只对本文件有效,如果导入其他文件,版本标识不会被导入,启动编译器检查

^0.5.2;  #从0.5.2到0.6(不含)的版本
import 导入文件
import * as symbolName from "filename";  #等价
import "filename" as symbolName;

状态变量

状态变量是永久地存储在合约存储中的,有基本类型.函数外的都是store状态变量。

  • bool,
  • int/uint(有符号和无符号)
  • fixed / ufixed (有符号和无符号的定长浮点型。)
在关键字 ufixedMxN 和 fixedMxN 中,M 表示该类型占用的位数,N 表示可用的小数位数。 M 必须能整除 8,即 8 到 256 位。 N 则可以是从 0 到 80 之间的任意数。 ufixed 和 fixed 分别是 ufixed128x19 和 fixed128x19 的别名
  • address 地址类型
address:保存一个20字节的值(以太坊地址的大小)。
address payable :可支付地址,与 address 相同,不过有成员函数 transfer 和 send
address payable 可以完成到 address 的隐式转换,但是从 address 到 address payable 必须显示的转换, 通过 payable(<address>) 进行转换
  • bytes1 定长字节数组

  • bytes和string,uint[] 变长字节数组

  • 多维数组的下标和一般是相反的,a[2][4]表示4个子数列,每个子数列里2个元素

函数

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

函数是代码的可执行单元。函数通常在合约内部定义,但也可以在合约外定义。

函数可以作为参数传入

可见性

https://solidity-by-example.org/visibility/

内部(internal) 函数类型,只能在当前合约内被调用

外部(external) 函数类型,由一个地址和函数签名组成,在调用时会被视作function类型

external: 外部函数作为合约接口的一部分,可以被交易或者其他合约调用。 外部函数 f 不能以内部调用的方式调用(即 f 不起作用,但 this.f() 可以)。

public: public 函数是合约接口的一部分,可以在内部或通过消息调用。对于 public 状态变量, 会自动生成一个 getter 函数。

internal : 只能在当前合约内部或它子合约中访问,不使用 this 调用。

private: private 函数和状态变量仅在当前定义它们的合约中使用,并且不能被派生合约使用(如继承)

有且仅有以下三种转化:

  • pure 函数可以转换为 viewnon-payable 函数
  • view 函数可以转换为 non-payable 函数
  • payable 函数可以转换为 non-payable 函数
参数和返回值
修饰符

https://solidity-by-example.org/function-modifier/

函数修饰符用来修饰函数,比如添加函数执行前必须的先决条件.函数修饰器通过继承在派生合约中起作用。

 modifier onlyOwner { 函数体会插入在修饰函数的下划线_的位置
      require(msg.sender == owner);
      _;
   }
如果一个函数中有许多修饰器,写法上以空格隔开,执行时依次执行:首先进入第一个函数修饰器,然后一直执行到_;接着跳转回函数体,进入第二个修饰器,以此类推。到达最后一层时,一次返回到上一层修饰器的_;后。
自由函数

定义在合约外的函数叫做自由函数,一定是internal类型,就像一个内部函数库一样,会包含在所有调用他们的合约内,就像写在对应位置一样。但是自由函数不能直接访问全局变量和其他不在作用域下的函数(比如,需要通过地址引入合约,再使用合约内的函数)

view

view函数不能产生任何修改。由于操作码的原因,view库函数不会在运行时阻止状态改变,不过编译时静态检查器会发现这个问题。

以下行为都视为修改状态:

  1. 修改状态变量。
  2. 触发事件。
  3. 创建其它合约。
  4. 使用 selfdestruct
  5. 通过调用发送以太币。
  6. 调用任何没有标记为 view 或者 pure 的函数。
  7. 使用低级调用。
  8. 使用包含特定操作码的内联汇编。
receive 函数

一个合约至多有一个receive函数,形如receive() external payable { ... } ,注意没有function 的标识,没有参数,只能是externalpayable标识,可以有函数修饰器,支持重载。

receive函数在没有任何调用数据时执行(如用.send()或者.transfer()给合约转账),如果没有设置receive函数,那么就会执行fallback函数,如果这两个函数都不存在,合约就不能通过交易的形式获取以太币

回退函数

一个合约至多一个回退函数,格式如:fallback () external [payable] 或者 fallback (bytes calldata _input) external [payable] returns (bytes memory _output),后者的函数参数会接收完整的调用信息(msg.data),返回的时未经修改的数据(如为经过ABI编码)。

回退函数可以时virtual的,可以重载,也可以被修饰器修饰。在函数调用时,如果没有与之匹配的函数签名或者消息调用为空且无receive函数,就会调用fallbakc函数。

如果回退函数代替了receive函数,那么仍然只有2300gas可用。

构造函数
contract X {
    string public name;
    constructor(string memory _name) {
        name = _name;
    }
}
// Base contract Y
contract Y {
    string public text;
    constructor(string memory _text) {
        text = _text;
    }
}
两种方式
contract B is X("Input to X"), Y("Input to Y") {
}
// Order of constructors called:
// 1. X
// 2. Y
// 3. C
contract C is X, Y {
    // Pass the parameters here in the constructor,
    // similar to function modifiers.
    constructor(string memory _name, string memory _text) X(_name) Y(_text) {}
}

事件

事件是能方便地调用以太坊虚拟机日志功能的接口,分为设置事件和触发事件

pragma solidity >=0.4.21 <0.9.0;
contract TinyAuction {
    event HighestBidIncreased(address bidder, uint amount); // 事件

    function bid() public payable {
        // ...
        emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
    }
}

事件是对EVM日志的简短总结,可以通过RPC接口监听。触发事件时,设置好的参数就会记录在区块链的交易日志中,永久的保存,但是合约本身是不可以访问这些日志的。可以通过带有日志的Merkle证明的合约,来检查日志是否存在于区块链上。由于合约中仅能访问最近的 256 个区块哈希,所以还需要提供区块头信息。

继承

https://solidity-by-example.org/inheritance/

当合约继承其他的合约时,只会在区块链上生成一个合约,所有相关的合约都会编译进这个合约,调用机制和写在一个合约上一致。

所以如果继承了多个合约,希望把所有的同名函数都执行一遍,就需要super关键词。

函数重写

父合约中被标记为virtual的非private函数可以在子合约中用override重写。

重写可以改变函数的标识符,规则如下:

  • 可见性只能单向从 external 更改为 public。
  • nonpayable 可以被 viewpure 覆盖。
  • view 可以被 pure 覆盖。
  • payable 不可被覆盖。
  • 函数修饰器也支持重写,且和函数重写规则一致
// Contracts inherit other contracts by using the keyword 'is'.
contract B is A {
    // Override A.foo()
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    // Override A.foo()
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}
contract D is B, C {
    // D.foo() returns "C"  执行最远的那个父类函数C的调用
    // since C is the right most parent contract with function foo()
    function foo() public pure override(B, C) returns (string memory) {
        return super.foo();
    }
}

接口

接口和抽象合约的作用很类似,但是它的每一个函数都没有实现,而且不可以作为其他合约的子合约,只能作为父合约被继承。

接口中所有的函数必须是external,且不包含构造函数和全局变量。接口的所有函数都会隐式标记为external,可以重写。但是多次重写的规则和多继承的规则和一般函数重写规则一致。

  • 不能实现任何功能
  • 可以从其他接口继承
  • 所有声明的函数必须是外部的
  • 不能声明构造函数
  • 不能声明状态变量

interface ICounter {
    function count() external view returns (uint);
    function increment() external;
}

contract MyContract {
    function incrementCounter(address _counter) external {
        ICounter(_counter).increment();
    }
    function getCount(address _counter) external view returns (uint) {
        return ICounter(_counter).count();
    }
}

引用类型

引用类型可以通过不同变量名来修改指向的同一个值。目前的引用类型包括:结构体、数组和映射

  • memory:存储在内存里,只在函数内部使用,函数内不做特殊说明为memory类型。
  • storage:相当于全局变量。函数外合约内的都是storage类型。
  • calldata:保存有函数的参数,不可修改,大多数时候和memory相似。它常作为外部函数的参数,也可以当作其他的变量使用
  • 尽可能使用calldata,因为它既不会复制,也不能修改,而且还可以作为函数的返回值
  • storagememory之间的赋值或者用calldata对它们赋值,都是产生独立的拷贝,不修改原来的值,其他向storage 赋值是拷贝,结构体里面的赋值是一个拷贝。
  • 于数组和结构体在函数中要有存储位置声明

基本类型转换

隐式转换:隐式转换发生在编译时期,如果不出现信息丢失,其实都可以进行隐式转换,比如uint8可以转成uint16。隐式转换常发生在不同的操作数一起用操作符操作时发生。

显式转换:如果编译器不允许隐式转换,而你足够自信没问题,那么就去尝试显示转换,但是这很容易造成安全问题

如果是uint或者int同类型强制转换,就是从最低位截断

单位、内置函数和变量

  • 币的单位默认是wei,也可以添加后缀。
1 wei == 1;
1 gwei == 1e9;
1 ether == 1e18;

区块和交易属性

  • blockhash(uint blockNumber) returns (bytes32):指定区块的区块哈希,但是仅可用于最新的 256 个区块且不包括当前区块,否则返回0.

  • block.chainid (uint): 当前链 id

  • block.coinbase ( address ): 挖出当前区块的矿工地址

  • block.difficulty ( uint ): 当前区块难度

  • block.gaslimit ( uint ): 当前区块 gas 限额

  • block.number ( uint ): 当前区块号

  • block.timestamp ( uint): 自 unix epoch 起始当前区块以秒计的时间戳

  • gasleft() returns (uint256) :剩余的 gas

  • msg.data ( bytes ): 完整的 calldata

  • msg.sender ( address ): 消息发送者(当前调用)

  • msg.sig ( bytes4 ): calldata 的前 4 字节(也就是函数标识符)

  • msg.value ( uint ): 随消息发送的 wei 的数量

  • tx.gasprice (uint): 交易的 gas 价格

  • tx.origin (address payable): 交易发起者(完全的调用链)

    错误处理

    assert(bool condition),require(bool condition),require(bool condition, string memory message)均是条件为假然后回滚。

    revert(),revert(string memory reason) 立即回滚

内部调用

内部调用再EVM中只是简单的跳转,传递当前的内存的引用,效率很高。但是仍然要避免过多的递归,因为每次进入内部函数都会占用一个堆栈槽,而最多只有1024个堆栈槽。

外部调用

  • 只有external或者public的函数才可以通过消息调用而不是单纯的跳转调用,外部函数的参数会暂时复制在内存中。
  • 注意this不可以出现在构造函数里,因为此时合约还没有完成。
  • 调用时可以指定 value 和 gas 。这里导入合约使用的时初始化合约实例然后赋予地址。

元组的赋值行为

函数的返回值可以是元组,因此就可以用元组的形式接收,但是必须按照顺序排列。在0.5.0之后,两个元组的大小必须相同,用逗号表示间隔,可以空着省略元素。注意,不允许赋值和声明都出现在元组里,比如(x, uint y) = (1, 2);不合法。

元祖就是多个不同类型数据组成的数组

错误

assert函数,用于检查内部错误,返回Panic(uint256),错误代码分别表示:

  1. 0x00: 由编译器本身导致的Panic.
  2. 0x01: assert 的参数(表达式)结果为 false 。
  3. 0x11: 在unchecked { … }外,算术运算结果向上或向下溢出。
  4. 0x12: 除以0或者模0.
  5. 0x21: 不合适的枚举类型转换。
  6. 0x22: 访问一个没有正确编码的storagebyte数组.
  7. 0x31: 对空数组 .pop()
  8. 0x32: 数组的索引越界或为负数。
  9. 0x41: 分配了太多的内存或创建的数组过大。
  10. 0x51: 如果你调用了零初始化内部函数类型变量

Error(string)的异常由编译器产生,有以下情况:

  1. require 的参数为 false
  2. 触发revert或者revert("discription")
  3. 执行外部函数调用合约没有代码。
  4. payable 修饰的函数(包括构造函数和 fallback 函数),接收以太币。
  5. 合约通过 getter 函数接收以太币 。

try/catch
try后面只能接外部函数调用或者是创建合约new ContractName的表达式,并且表达式内部的错误并不会被记录,只有调用的外部函数内出现错误才会返回回滚的信息。如果有returns的话,后面接外部函数的返回值类型,return后面是当前合约函数的返回值。

地址

以太坊地址是给定公钥哈希的最后 20 个字节。使用的散列算法是Keccak-256。所以对于一个唯一的私钥 => 唯一的哈希.

所有钱包都应该接受以大写或小写字符表示的以太坊地址

  • 您可以将 Ether 发送到定义为的变量address payable

  • 您不能将 Ether 发送到定义为的变量address

  • msg.sender() -> Returns : address payable

  • tx.origin() -> Returns : address payable

  • .transfer()` , `.send()` , `.call()` ,
    `.delegatecall()` and `.staticcall() 
    是address payable 定义变量的用法
    
  • address 具有balance方法,表示eth余额

  • address payable` to `address 隐式转换可以,返过来不可以
    
    contract NotPayable {
    }
    contract Payable {
         function() payable {}
    }
    contract HelloWorld {
         address x = address(NotPayable); //address类型
         address y = address(Payable); //address payable类型,因为该合同有payable类型的回调函数
         function hello_world() public pure returns (string memory) {
              return "hello world";
         }
    }
    

EVM 提供了4种 特殊的操作码来与其他智能合约交互,其中 3 种可用作以下address类型的方法:**call****delegatecode****staticcall**

call:

https://solidity-by-example.org/call/

address.call(bytes memory) returns (bool, bytes memory)

我是合约 A,我想为合约 B 存储执行合约 B 功能。调用 B.function() 只能更新 B 的存储

callcode
address.callcode(__payload__)

*.callcode()*现在已弃用,取而代之的是*.delegatecall()*. 但是,仍然可以在内联汇编中使用它。

合约A本质上是复制B的功能

我是合约 A,我想为我的存储执行合约 B 的功能。调用 B.function() 将更新 A 的存储

delegatecall

https://solidity-by-example.org/delegatecall/

_address.**delegatecall(**bytes memory**)** returns (bool, bytes memory)

我是合约A,我想执行合约B的功能,但是合约B可以伪装成我。

B 的函数可以覆盖 A 的存储,并在任何其他合约中伪装成A。

msg.sender 将是 A 的地址,而不是 B

在这种情况下,合约 A 本质上是将函数调用委托给 B。与前callcode一种方法的不同之处在于,使用delegatecallnot only enable 覆盖合约 A 的存储。

如果合约 B 调用另一个合约 C,合约 C 将看到它msg.sender是合约 A

把B的代码在A的执行环境中执行,数据使用A的

staticcall
address.**staticcall(**bytes memory**)** returns (bool, bytes memory)

规格

  • 低级STATICCALL,请参阅操作码 OxF4)(具有当前合约看到的完整 msg 上下文)给定作为参数传递的memory(数据有效负载)。
  • 返回一个元组:

当交易在接收者字段中指定一个称为零地址的特定地址时,它打算创建一个新合约.向该地址发送资金实际上不会转移任何以太币。在以太坊网络中,矿工将包含此接收者的交易解释为创建新智能合约的指令。

应付

用来接收和提取eth

contract Payable {
    // Payable address can receive Ether
    address payable public owner;

    // Payable constructor can receive Ether
    constructor() payable {
        owner = payable(msg.sender);
    }
        //把合约里的amount提取到owener地址上啊,call在那个地址上调用,就更新那个地址的数据
    // Function to withdraw all Ether from this contract.
    function withdraw() public {
        // get the amount of Ether stored in this contract
        uint amount = address(this).balance;

        // send all Ether to owner
        // Owner can receive Ether since the address of owner is payable
        (bool success, ) = owner.call{value: amount}("");
        require(success, "Failed to send Ether");
    }

    // Function to transfer Ether from this contract to address from input
    function transfer(address payable _to, uint _amount) public {
        // Note that "to" is declared as payable
        (bool success, ) = _to.call{value: _amount}("");
        require(success, "Failed to send Ether");
    }
}
contract ReceiveEther { 接受者接收的逻辑
    /*
    Which function is called, fallback() or receive()?

           send Ether
               |
         msg.data is empty?
              / \
            yes  no
            /     \
receive() exists?  fallback()
         /   \
        yes   no
        /      \
    receive()   fallback()
    */

    // Function to receive Ether. msg.data must be empty
    receive() external payable {}

    // Fallback function is called when msg.data is not empty
    fallback() external payable {}

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract SendEther {
消息调用者把比发给to这个地址
    function sendViaTransfer(address payable _to) public payable {
        // This function is no longer recommended for sending Ether.
        _to.transfer(msg.value);
    }
消息调用者把比发给to这个地址,需要发送成功,
    function sendViaSend(address payable _to) public payable {
        // Send returns a boolean value indicating success or failure.
        // This function is not recommended for sending Ether.
        bool sent = _to.send(msg.value);
        require(sent, "Failed to send Ether");
    }
消息调用者把比发给to这个地址,需要发送成功,
    function sendViaCall(address payable _to) public payable {
        // Call returns a boolean value indicating success or failure.
        // This is the current recommended method to use.
        (bool sent, bytes memory data) = _to.call{value: msg.value}("");
        require(sent, "Failed to send Ether");
    }
}

回调函数fallback

fallback是一个不接受任何参数且不返回任何内容的函数。

它在何时执行

  • 调用不存在的函数或
  • 以太币直接发送到合约但receive()不存在或msg.data不为空
  • fallback被transfer or send.调用时有 2300 气体限制

功能选择器

调用函数时,前 4 个字节calldata指定调用哪个函数。

下面的这段代码。它用于在地址上call执行transfer合约addr。
addr.call(abi.encodeWithSignature("transfer(address,uint256)", 0xSomeAddress, 123))

调用其他合约

https://solidity-by-example.org/calling-contract/

最简单的方法就是直接调用它,比如A.foo(x, y, z).

调用其他合约的另一种方法是使用低级call.

try/Catch

contract Foo {
    address public owner;

    constructor(address _owner) {
        require(_owner != address(0), "invalid address");
        assert(_owner != 0x0000000000000000000000000000000000000001);
        owner = _owner;
    }

    function myFunc(uint x) public pure returns (string memory) {
        require(x != 0, "require failed");
        return "my func was called";
    }
}

contract Bar {
    event Log(string message);
    event LogBytes(bytes data);
     function tryCatchNewContract(address _owner) public {
        try new Foo(_owner) returns (Foo foo) {
            // you can use variable foo here
            emit Log("Foo created");
        } catch Error(string memory reason) {
            // catch failing revert() and require()
            emit Log(reason);
        } catch (bytes memory reason) {
            // catch failing assert()
            emit LogBytes(reason);
        }
    }
}    

ERC20

任何遵循ERC20 标准的合约都是 ERC20 代币。该标准提供了代币的基本功能:如转移代币,授权代币给其他人(如链上第三方应用)使用。标准接口允许以太坊上的任何代币被其他应用程序重用,如钱包、去中心化交易所等

这是一个发币的合约,平日里所接触的许许多多代币如usdt(erc20)、usdc、dai、unsiwap、chainlink、wbtc、sushi等等绝大都数都是erc20代币,

ERC20 代币提供以下功能

  • 转移代币
  • 允许其他人代表代币持有者转移代币
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/token/ERC20/IERC20.sol
interface IERC20 {
    function totalSupply() external view returns (uint);

    function balanceOf(address account) external view returns (uint);

    function transfer(address recipient, uint amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint amount) external returns (bool);

    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,442评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,604评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,576评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,652评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,495评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,370评论 1 274
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,792评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,435评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,735评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,777评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,553评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,399评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,806评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,038评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,330评论 1 253
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,766评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,964评论 2 337

推荐阅读更多精彩内容