github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/docs/how-to-guides/porting-solidity-to-gno.md (about) 1 --- 2 id: port-solidity-to-gno 3 --- 4 5 # Port a Solidity Contract to a Gno Realm 6 7 8 ## Overview 9 10 This guide shows you how to port a Solidity contract `Simple Auction` to a Gno Realm `auction.gno` with test cases (Test Driven Development (TDD) approach). 11 12 You can check the Solidity contract in this [link](https://docs.soliditylang.org/en/latest/solidity-by-example.html#simple-open-auction), and here's the code for porting. 13 14 ```solidity 15 // SPDX-License-Identifier: GPL-3.0 16 pragma solidity ^0.8.4; 17 contract SimpleAuction { 18 // Parameters of the auction. Times are either 19 // absolute unix timestamps (seconds since 1970-01-01) 20 // or time periods in seconds. 21 address payable public beneficiary; 22 uint public auctionEndTime; 23 24 // Current state of the auction. 25 address public highestBidder; 26 uint public highestBid; 27 28 // Allowed withdrawals of previous bids 29 mapping(address => uint) pendingReturns; 30 31 // Set to true at the end, disallows any change. 32 // By default initialized to `false`. 33 bool ended; 34 35 // Events that will be emitted on changes. 36 event HighestBidIncreased(address bidder, uint amount); 37 event AuctionEnded(address winner, uint amount); 38 39 // Errors that describe failures. 40 41 // The triple-slash comments are so-called natspec 42 // comments. They will be shown when the user 43 // is asked to confirm a transaction or 44 // when an error is displayed. 45 46 /// The auction has already ended. 47 error AuctionAlreadyEnded(); 48 /// There is already a higher or equal bid. 49 error BidNotHighEnough(uint highestBid); 50 /// The auction has not ended yet. 51 error AuctionNotYetEnded(); 52 /// The function auctionEnd has already been called. 53 error AuctionEndAlreadyCalled(); 54 55 /// Create a simple auction with `biddingTime` 56 /// seconds bidding time on behalf of the 57 /// beneficiary address `beneficiaryAddress`. 58 constructor( 59 uint biddingTime, 60 address payable beneficiaryAddress 61 ) { 62 beneficiary = beneficiaryAddress; 63 auctionEndTime = block.timestamp + biddingTime; 64 } 65 66 /// Bid on the auction with the value sent 67 /// together with this transaction. 68 /// The value will only be refunded if the 69 /// auction is not won. 70 function bid() external payable { 71 // No arguments are necessary, all 72 // information is already part of 73 // the transaction. The keyword payable 74 // is required for the function to 75 // be able to receive Ether. 76 77 // Revert the call if the bidding 78 // period is over. 79 if (block.timestamp > auctionEndTime) 80 revert AuctionAlreadyEnded(); 81 82 // If the bid is not higher, send the 83 // money back (the revert statement 84 // will revert all changes in this 85 // function execution including 86 // it having received the money). 87 if (msg.value <= highestBid) 88 revert BidNotHighEnough(highestBid); 89 90 if (highestBid != 0) { 91 // Sending back the money by simply using 92 // highestBidder.send(highestBid) is a security risk 93 // because it could execute an untrusted contract. 94 // It is always safer to let the recipients 95 // withdraw their money themselves. 96 pendingReturns[highestBidder] += highestBid; 97 } 98 highestBidder = msg.sender; 99 highestBid = msg.value; 100 emit HighestBidIncreased(msg.sender, msg.value); 101 } 102 103 /// Withdraw a bid that was overbid. 104 function withdraw() external returns (bool) { 105 uint amount = pendingReturns[msg.sender]; 106 if (amount > 0) { 107 // It is important to set this to zero because the recipient 108 // can call this function again as part of the receiving call 109 // before `send` returns. 110 pendingReturns[msg.sender] = 0; 111 112 // msg.sender is not of type `address payable` and must be 113 // explicitly converted using `payable(msg.sender)` in order 114 // use the member function `send()`. 115 if (!payable(msg.sender).send(amount)) { 116 // No need to call throw here, just reset the amount owing 117 pendingReturns[msg.sender] = amount; 118 return false; 119 } 120 } 121 return true; 122 } 123 124 /// End the auction and send the highest bid 125 /// to the beneficiary. 126 function auctionEnd() external { 127 // It is a good guideline to structure functions that interact 128 // with other contracts (i.e. they call functions or send Ether) 129 // into three phases: 130 // 1. checking conditions 131 // 2. performing actions (potentially changing conditions) 132 // 3. interacting with other contracts 133 // If these phases are mixed up, the other contract could call 134 // back into the current contract and modify the state or cause 135 // effects (ether payout) to be performed multiple times. 136 // If functions called internally include interaction with external 137 // contracts, they also have to be considered interaction with 138 // external contracts. 139 140 // 1. Conditions 141 if (block.timestamp < auctionEndTime) 142 revert AuctionNotYetEnded(); 143 if (ended) 144 revert AuctionEndAlreadyCalled(); 145 146 // 2. Effects 147 ended = true; 148 emit AuctionEnded(highestBidder, highestBid); 149 150 // 3. Interaction 151 beneficiary.transfer(highestBid); 152 } 153 } 154 ``` 155 156 These are the basic concepts of the Simple Auction contract: 157 158 * Everyone can send their bids during a bidding period. 159 * The bids already include sending money / Ether in order to bind the bidders to their bids. 160 * If the highest bid is raised, the previous highest bidder gets their money back. 161 * After the end of the bidding period, the contract has to be called manually for the beneficiary to receive their money - contracts cannot activate themselves. 162 163 The contract consists of: 164 165 * A variable declaration 166 * Initialization by a constructor 167 * Three functions 168 169 Let's dive into the details of the role of each function, and learn how to port each function into Gno with test cases. 170 171 When writing a test case, the following conditions are often used to determine whether the function has been properly executed: 172 173 * Value matching 174 * Error status 175 * Panic status 176 177 Below is a test case helper that will help implement each condition. 178 179 ### Gno - Testcase Helper 180 181 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-1.gno go) 182 ```go 183 func shouldEqual(t *testing.T, got interface{}, expected interface{}) { 184 t.Helper() 185 186 if got != expected { 187 t.Errorf("expected %v(%T), got %v(%T)", expected, expected, got, got) 188 } 189 } 190 191 func shouldErr(t *testing.T, err error) { 192 t.Helper() 193 if err == nil { 194 t.Errorf("expected an error, but got nil.") 195 } 196 } 197 198 func shouldNoErr(t *testing.T, err error) { 199 t.Helper() 200 if err != nil { 201 t.Errorf("expected no error, but got err: %s.", err.Error()) 202 } 203 } 204 205 func shouldPanic(t *testing.T, f func()) { 206 defer func() { 207 if r := recover(); r == nil { 208 t.Errorf("should have panic") 209 } 210 }() 211 f() 212 } 213 214 func shouldNoPanic(t *testing.T, f func()) { 215 defer func() { 216 if r := recover(); r != nil { 217 t.Errorf("should not have panic") 218 } 219 }() 220 f() 221 } 222 ``` 223 224 ## Variable init - Solidity 225 226 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-2.sol solidity) 227 ```solidity 228 // Parameters of the auction. Times are either 229 // absolute unix timestamps (seconds since 1970-01-01) 230 // or time periods in seconds. 231 address payable public beneficiary; 232 uint public auctionEndTime; 233 234 // Current state of the auction. 235 address public highestBidder; 236 uint public highestBid; 237 238 // Allowed withdrawals of previous bids 239 mapping(address => uint) pendingReturns; 240 241 // Set to true at the end, disallows any change. 242 // By default initialized to `false`. 243 bool ended; 244 245 // Events that will be emitted on changes. 246 event HighestBidIncreased(address bidder, uint amount); 247 event AuctionEnded(address winner, uint amount); 248 249 // Errors that describe failures. 250 251 // The triple-slash comments are so-called natspec 252 // comments. They will be shown when the user 253 // is asked to confirm a transaction or 254 // when an error is displayed. 255 256 /// The auction has already ended. 257 error AuctionAlreadyEnded(); 258 /// There is already a higher or equal bid. 259 error BidNotHighEnough(uint highestBid); 260 /// The auction has not ended yet. 261 error AuctionNotYetEnded(); 262 /// The function auctionEnd has already been called. 263 error AuctionEndAlreadyCalled(); 264 265 /// Create a simple auction with `biddingTime` 266 /// seconds bidding time on behalf of the 267 /// beneficiary address `beneficiaryAddress`. 268 constructor( 269 uint biddingTime, 270 address payable beneficiaryAddress 271 ) { 272 beneficiary = beneficiaryAddress; 273 auctionEndTime = block.timestamp + biddingTime; 274 } 275 ``` 276 277 * `address payable public beneficiary;` : Address to receive the amount after the auction's ending. 278 * `uint public auctionEndTime;` : Auction ending time. 279 * `address public highestBidder;` : The highest bidder. 280 * `uint public highestBid;` : The highest bid. 281 * `mapping(address => uint) pendingReturns;` : Bidder's address and amount to be returned (in case of the highest bid changes). 282 * `bool ended;` : Whether the auction is closed. 283 284 ### Variable init - Gno 285 286 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-3.gno go) 287 ```go 288 var ( 289 receiver = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") 290 auctionEndBlock = std.GetHeight() + uint(300) // in blocks 291 highestBidder std.Address 292 highestBid = uint(0) 293 pendingReturns avl.Tree 294 ended = false 295 ) 296 ``` 297 298 > **Note:** In Solidity, the Auction ending time is set by a time basis, but in the above case, it's set by a block basis. 299 300 ### 301 302 ## bid() - Solidity 303 304 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-4.sol solidity) 305 ```solidity 306 function bid() external payable { 307 // No arguments are necessary, all 308 // information is already part of 309 // the transaction. The keyword payable 310 // is required for the function to 311 // be able to receive Ether. 312 313 // Revert the call if the bidding 314 // period is over. 315 if (block.timestamp > auctionEndTime) 316 revert AuctionAlreadyEnded(); 317 318 // If the bid is not higher, send the 319 // money back (the revert statement 320 // will revert all changes in this 321 // function execution including 322 // it having received the money). 323 if (msg.value <= highestBid) 324 revert BidNotHighEnough(highestBid); 325 326 if (highestBid != 0) { 327 // Sending back the money by simply using 328 // highestBidder.send(highestBid) is a security risk 329 // because it could execute an untrusted contract. 330 // It is always safer to let the recipients 331 // withdraw their money themselves. 332 pendingReturns[highestBidder] += highestBid; 333 } 334 highestBidder = msg.sender; 335 highestBid = msg.value; 336 emit HighestBidIncreased(msg.sender, msg.value); 337 } 338 ``` 339 340 `bid()` function is for participating in an auction and includes: 341 342 * Determining whether an auction is closed. 343 * Comparing a new bid with the current highest bid. 344 * Prepare data to return the bid amount to the existing highest bidder in case of the highest bid is increased. 345 * Update variables with the top bidder & top bid amount. 346 347 ### bid() - Gno 348 349 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-5.gno go) 350 ```go 351 func Bid() { 352 if std.GetHeight() > auctionEndBlock { 353 panic("Exceeded auction end block") 354 } 355 356 sentCoins := std.GetOrigSend() 357 if len(sentCoins) != 1 { 358 panic("Send only one type of coin") 359 } 360 361 sentAmount := uint(sentCoins[0].Amount) 362 if sentAmount <= highestBid { 363 panic("Too few coins sent") 364 } 365 366 // A new bid is higher than the current highest bid 367 if sentAmount > highestBid { 368 // If the highest bid is greater than 0, 369 if highestBid > 0 { 370 // Need to return the bid amount to the existing highest bidder 371 // Create an AVL tree and save 372 pendingReturns.Set(highestBidder.String(), highestBid) 373 } 374 375 // Update the top bidder address 376 highestBidder = std.GetOrigCaller() 377 // Update the top bid amount 378 highestBid = sentAmount 379 } 380 } 381 ``` 382 383 ### bid() - Gno Testcase 384 385 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-6.gno go) 386 ```go 387 // Bid Function Test - Send Coin 388 func TestBidCoins(t *testing.T) { 389 // Sending two types of coins 390 std.TestSetOrigCaller(bidder01) 391 std.TestSetOrigSend(std.Coins{{"ugnot", 0}, {"test", 1}}, nil) 392 shouldPanic(t, Bid) 393 394 // Sending lower amount than the current highest bid 395 std.TestSetOrigCaller(bidder01) 396 std.TestSetOrigSend(std.Coins{{"ugnot", 0}}, nil) 397 shouldPanic(t, Bid) 398 399 // Sending more amount than the current highest bid (exceeded) 400 std.TestSetOrigCaller(bidder01) 401 std.TestSetOrigSend(std.Coins{{"ugnot", 1}}, nil) 402 shouldNoPanic(t, Bid) 403 } 404 405 // Bid Function Test - Bid by two or more people 406 func TestBidCoins(t *testing.T) { 407 // bidder01 bidding with 1 coin 408 std.TestSetOrigCaller(bidder01) 409 std.TestSetOrigSend(std.Coins{{"ugnot", 1}}, nil) 410 shouldNoPanic(t, Bid) 411 shouldEqual(t, highestBid, 1) 412 shouldEqual(t, highestBidder, bidder01) 413 shouldEqual(t, pendingReturns.Size(), 0) 414 415 // bidder02 bidding with 1 coin 416 std.TestSetOrigCaller(bidder02) 417 std.TestSetOrigSend(std.Coins{{"ugnot", 1}}, nil) 418 shouldPanic(t, Bid) 419 420 // bidder02 bidding with 2 coins 421 std.TestSetOrigCaller(bidder02) 422 std.TestSetOrigSend(std.Coins{{"ugnot", 2}}, nil) 423 shouldNoPanic(t, Bid) 424 shouldEqual(t, highestBid, 2) 425 shouldEqual(t, highestBidder, bidder02) 426 shouldEqual(t, pendingReturns.Size(), 1) 427 } 428 ``` 429 430 ### 431 432 ## withdraw() - Solidity 433 434 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-7.sol solidity) 435 ```solidity 436 /// Withdraw a bid that was overbid. 437 function withdraw() external returns (bool) { 438 uint amount = pendingReturns[msg.sender]; 439 if (amount > 0) { 440 // It is important to set this to zero because the recipient 441 // can call this function again as part of the receiving call 442 // before `send` returns. 443 pendingReturns[msg.sender] = 0; 444 445 // msg.sender is not of type `address payable` and must be 446 // explicitly converted using `payable(msg.sender)` in order 447 // use the member function `send()`. 448 if (!payable(msg.sender).send(amount)) { 449 // No need to call throw here, just reset the amount owing 450 pendingReturns[msg.sender] = amount; 451 return false; 452 } 453 } 454 return true; 455 } 456 ``` 457 458 `withdraw()` is to return the bid amount to the existing highest bidder in case of the highest bid changes and includes: 459 460 * When called, determine if there's a bid amount to be returned to the address. 461 * (If there's an amount to be returned) Before returning, set the previously recorded amount to `0` and return the actual amount. 462 463 ### withdraw() - Gno 464 465 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-8.gno go) 466 ```go 467 func Withdraw() { 468 // Query the return amount to non-highest bidders 469 amount, _ := pendingReturns.Get(std.GetOrigCaller().String()) 470 471 if amount > 0 { 472 // If there's an amount, reset the amount first, 473 pendingReturns.Set(std.GetOrigCaller().String(), 0) 474 475 // Return the exceeded amount 476 banker := std.GetBanker(std.BankerTypeRealmSend) 477 pkgAddr := std.GetOrigPkgAddr() 478 479 banker.SendCoins(pkgAddr, std.GetOrigCaller(), std.Coins{{"ugnot", amount.(int64)}}) 480 } 481 } 482 ``` 483 484 ### 485 486 ### withdraw() - Gno Testcase 487 488 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-9.gno go) 489 ```go 490 // Withdraw Function Test 491 func TestWithdraw(t *testing.T) { 492 // If there's no participants for return 493 shouldEqual(t, pendingReturns.Size(), 0) 494 495 // If there's participants for return (data generation 496 returnAddr := bidder01.String() 497 returnAmount := int64(3) 498 pendingReturns.Set(returnAddr, returnAmount) 499 shouldEqual(t, pendingReturns.Size(), 1) 500 shouldEqual(t, pendingReturns.Has(returnAddr), true) 501 502 banker := std.GetBanker(std.BankerTypeRealmSend) 503 pkgAddr := std.GetOrigPkgAddr() 504 banker.SendCoins(pkgAddr, std.Address(returnAddr), std.Coins{{"ugnot", returnAmount}}) 505 shouldEqual(t, banker.GetCoins(std.Address(returnAddr)).String(), "3ugnot") 506 } 507 ``` 508 509 ## auctionEnd() - Solidity 510 511 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-10.sol solidity) 512 ```solidity 513 /// End the auction and send the highest bid 514 /// to the beneficiary. 515 function auctionEnd() external { 516 // It is a good guideline to structure functions that interact 517 // with other contracts (i.e. they call functions or send Ether) 518 // into three phases: 519 // 1. checking conditions 520 // 2. performing actions (potentially changing conditions) 521 // 3. interacting with other contracts 522 // If these phases are mixed up, the other contract could call 523 // back into the current contract and modify the state or cause 524 // effects (ether payout) to be performed multiple times. 525 // If functions called internally include interaction with external 526 // contracts, they also have to be considered interaction with 527 // external contracts. 528 529 // 1. Conditions 530 if (block.timestamp < auctionEndTime) 531 revert AuctionNotYetEnded(); 532 if (ended) 533 revert AuctionEndAlreadyCalled(); 534 535 // 2. Effects 536 ended = true; 537 emit AuctionEnded(highestBidder, highestBid); 538 539 // 3. Interaction 540 beneficiary.transfer(highestBid); 541 } 542 ``` 543 544 `auctionEnd()` function is for ending the auction and includes: 545 546 * Determines if the auction should end by comparing the end time. 547 * Determines if the auction has already ended or not. 548 * (If not ended) End the auction. 549 * (If not ended) Send the highest bid amount to the recipient. 550 551 ### auctionEnd() - Gno 552 553 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-11.gno go) 554 ```go 555 func AuctionEnd() { 556 if std.GetHeight() < auctionEndBlock { 557 panic("Auction hasn't ended") 558 } 559 560 if ended { 561 panic("Auction has ended") 562 563 } 564 ended = true 565 566 // Send the highest bid to the recipient 567 banker := std.GetBanker(std.BankerTypeRealmSend) 568 pkgAddr := std.GetOrigPkgAddr() 569 570 banker.SendCoins(pkgAddr, receiver, std.Coins{{"ugnot", int64(highestBid)}}) 571 } 572 ``` 573 574 ### auctionEnd() - Gno Testcase 575 576 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-12.gno go) 577 ```go 578 // AuctionEnd() Function Test 579 func TestAuctionEnd(t *testing.T) { 580 // Auction is ongoing 581 shouldPanic(t, AuctionEnd) 582 583 // Auction ends 584 highestBid = 3 585 std.TestSkipHeights(500) 586 shouldNoPanic(t, AuctionEnd) 587 shouldEqual(t, ended, true) 588 589 banker := std.GetBanker(std.BankerTypeRealmSend) 590 shouldEqual(t, banker.GetCoins(receiver).String(), "3ugnot") 591 592 // Auction has already ended 593 shouldPanic(t, AuctionEnd) 594 shouldEqual(t, ended, true) 595 } 596 ``` 597 598 ## Precautions for Running Test Cases 599 600 * Each test function should be executed separately one by one, to return all passes without any errors. 601 * Same as Go, Gno doesn't support `setup()` & `teardown()` functions. So running two or more test functions simultaneously can result in tainted data. 602 * If you want to do the whole test at once, make it into a single function as below: 603 604 [embedmd]:# (../assets/how-to-guides/porting-solidity-to-gno/porting-13.gno go) 605 ```go 606 // The whole test 607 func TestFull(t *testing.T) { 608 bidder01 := testutils.TestAddress("bidder01") // g1vf5kger9wgcrzh6lta047h6lta047h6lufftkw 609 bidder02 := testutils.TestAddress("bidder02") // g1vf5kger9wgcryh6lta047h6lta047h6lnhe2x2 610 611 // Variables test 612 { 613 shouldEqual(t, highestBidder, "") 614 shouldEqual(t, receiver, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") 615 shouldEqual(t, auctionEndBlock, 423) 616 shouldEqual(t, highestBid, 0) 617 shouldEqual(t, pendingReturns.Size(), 0) 618 shouldEqual(t, ended, false) 619 } 620 621 // Send two or more types of coins 622 { 623 std.TestSetOrigCaller(bidder01) 624 std.TestSetOrigSend(std.Coins{{"ugnot", 0}, {"test", 1}}, nil) 625 shouldPanic(t, Bid) 626 } 627 628 // Send less than the highest bid 629 { 630 std.TestSetOrigCaller(bidder01) 631 std.TestSetOrigSend(std.Coins{{"ugnot", 0}}, nil) 632 shouldPanic(t, Bid) 633 } 634 635 // Send more than the highest bid 636 { 637 std.TestSetOrigCaller(bidder01) 638 std.TestSetOrigSend(std.Coins{{"ugnot", 1}}, nil) 639 shouldNoPanic(t, Bid) 640 641 shouldEqual(t, pendingReturns.Size(), 0) 642 shouldEqual(t, highestBid, 1) 643 shouldEqual(t, highestBidder, "g1vf5kger9wgcrzh6lta047h6lta047h6lufftkw") 644 } 645 646 // Other participants in the auction 647 { 648 649 // Send less amount than the current highest bid (current: 1) 650 std.TestSetOrigCaller(bidder02) 651 std.TestSetOrigSend(std.Coins{{"ugnot", 1}}, nil) 652 shouldPanic(t, Bid) 653 654 // Send more amount than the current highest bid (exceeded) 655 std.TestSetOrigCaller(bidder02) 656 std.TestSetOrigSend(std.Coins{{"ugnot", 2}}, nil) 657 shouldNoPanic(t, Bid) 658 659 shouldEqual(t, highestBid, 2) 660 shouldEqual(t, highestBidder, "g1vf5kger9wgcryh6lta047h6lta047h6lnhe2x2") 661 662 shouldEqual(t, pendingReturns.Size(), 1) // Return to the existing bidder 663 shouldEqual(t, pendingReturns.Has("g1vf5kger9wgcrzh6lta047h6lta047h6lufftkw"), true) 664 } 665 666 // Auction ends 667 { 668 std.TestSkipHeights(150) 669 shouldPanic(t, AuctionEnd) 670 shouldEqual(t, ended, false) 671 672 std.TestSkipHeights(301) 673 shouldNoPanic(t, AuctionEnd) 674 shouldEqual(t, ended, true) 675 676 banker := std.GetBanker(std.BankerTypeRealmSend) 677 shouldEqual(t, banker.GetCoins(receiver).String(), "2ugnot") 678 } 679 } 680 ```